Skip to content

Kontinuum Pulse

Версия спецификации: v0.14.7 · Статус: as-built reference; роадмап и открытые вопросы вынесены в GitLab (см. issue #45) · Последнее обновление: 2026-05-20

Подсистема growth/monetization платформы Kontinuum. Принимает решения о маркетинговых, биллинговых, retention- и personalization-действиях на основе сочетания символьных правил, статистических моделей и языковых моделей (neuro-symbolic стек) с явным контролем приватности и аудируемости.

Pulse — это подсистема платформы, а не отдельный продукт для пользователя. Её клиенты: маркетинг-команда, billing, app-side personalization, regulatory audit.

Этот документ — рабочая спецификация. Открытые вопросы вынесены в GitLab (issue #45); формат большинства схем (контракты, метрики, DSL) намеренно описан абстрактно — конкретные сериализации фиксируются на этапе implementation в kontinuum-pulse/.


Оглавление

  1. Назначение
  2. Глоссарий
  3. Принципы и инварианты
  4. Архитектурный обзор
  5. Контрактный слой
  6. Telemetry contract
  7. Action contract
  8. Pipeline: 5 слоёв
  9. Cross-cutting горизонтали
  10. Места выполнения (deployment)
  11. Federation и согласование версий
  12. Модель угроз
  13. Интеграционные точки
  14. Версионирование и миграции
  15. Roadmap
  16. Открытые вопросы
  17. Marketing channels integration
  18. Testing strategy

1. Назначение

Kontinuum Pulse решает четыре связанные задачи:

  • Telemetry — сбор пользовательских событий из всех клиентских продуктов (App, Veil, Tether, Send) и Node-узлов в форме, пригодной для аналитики, ML и принятия решений, без нарушения E2E-инвариантов и privacy-обязательств.
  • Decisioning — принятие решений о маркетинговых, биллинговых, personalization-действиях на основе трёх независимых движков: символьного DSL правил (rules), статистических моделей (ML), языковой модели (LLM). Каждый движок отвечает за свой класс решений.
  • Experimentation — безопасное исследование пространства действий через bandit/A-B-эксперименты с обратной связью.
  • Audit & explainability — полная трассируемость каждого принятого решения с криптографической гарантией неизменности, пригодная для регуляторного аудита, ответа пользователю и внутреннего дебага.

Pulse не выполняет действий сам: он принимает решения и передаёт их в продуктовые системы (App, Billing, Node) через action contract. Эта граница — единственный способ влиять на пользователя.

Pulse не видит plaintext E2E-данных пользователя. Все события проходят через consent-фильтр и pseudonymization до того, как попадают во внутренние слои.

Чем Pulse не является

Не являетсяПочему
Заменой ops-метрикPrometheus/Grafana продолжают отвечать за SRE-метрики. Pulse — про бизнес-аналитику.
BI-инструментомЭто runtime-decisioning слой. Дашборды и отчёты — производные.
CRM-системойCRM (Directus в billing/) хранит клиентскую базу. Pulse использует её как input.
Платформой саппортаPulse может рекомендовать действия (например, эскалацию), но не управляет тикетами.
Аналитическим DWHPulse держит минимально необходимый feature store. Полный DWH — отдельная задача, может строиться поверх audit log в future.
Универсальным AI-агентомLLM в Pulse — это интерфейсный слой над декларативными артефактами, а не self-acting агент.

2. Глоссарий

ТерминОпределение
PulseПодсистема kontinuum-pulse, реализующая growth/monetization-петлю.
SubjectПсевдонимизированный идентификатор пользователя внутри Pulse. Не равен identity_id, device_id, pairing_id Kontinuum. Mapping держится единственным сервисом (см. §9.1).
EventИммутабельная запись о действии в продукте, эмитированная клиентом, edge node или central. Имеет тип, схему, consent-purpose, timestamp.
FeatureПроизводная величина, вычисленная из событий и/или других фич по правилам Semantic Layer. Используется как вход в ML и DSL.
MetricБизнес-определённая величина (например, active_user, churn_prob_7d), зарегистрированная в Semantic Layer. Метрика = производная функция от событий с явной семантикой.
Telemetry contractДекларативная схема всех типов событий: поля, типы, инварианты, consent-purpose-binding. Single source of truth для emit-сторон и Pulse-ingest.
Action contractДекларативная схема всех типов действий, которые Pulse может предложить продукту: имя, параметры, валидация, side-effect класс, идемпотентность.
ContractTelemetry contract или Action contract. Иммутабельный artifact, идентифицируемый content-hash, подписанный.
Contract channelИменованный канал распространения версий контракта (например, pulse/main). Указывает на текущую активную версию через signed pointer.
Consent purposeДекларативная цель использования данных (marketing.email, analytics.usage, personalization, experiments). Согласие даётся пер-purpose.
RuleДекларативный приём в Policy DSL: условие применимости + действие + приоритет. Каждое правило — атомарная единица growth-логики.
Policy DSLЯзык записи правил Pulse. Высокоуровневый, выполняется на нескольких runtime'ах (client, edge, central). Концептуальный наследник идеи «приёмов» Подколзина из neuro-symbolic подхода.
DecisionЗапись о принятом действии с полным контекстом: входы, активные правила, ML-скоринг, выбранный variant, обоснование. Иммутабельна, попадает в Audit Log.
Audit LogAppend-only лог решений с Merkle-цепочкой. Хранится централизованно, корни периодически публикуются в Kontinuum DHT для внешней верифицируемости.
Semantic LayerРеестр определений бизнес-терминов и метрик. Один YAML-источник, из которого генерируются типы для всех runtime'ов и SQL для feature store.
Feature StoreХранилище нормализованных фичей по subject и времени. Используется ML и DSL для чтения; пополняется ingest-слоем.
ExperimentКонтролируемое распределение subject'ов по variants с измерением метрик. Может быть классическим A/B или bandit'ом.
BanditАдаптивный эксперимент: распределение по variants обновляется по ходу на основе наблюдаемой reward-функции.
LLM GatewayПрокси-сервис в central, через который проходят все обращения к внешним LLM-провайдерам. Обрезает PII, кэширует, логирует.
Edge NodeЛюбой Kontinuum Node (Tier 0/1/2 из docs/node/architecture.md §3). Pulse использует Edge Nodes как relay для телеметрии и runtime для federation-policy DSL.
CentralКластер сервисов команды платформы: Pulse-core, Feature Store, ML training, LLM Gateway, Audit master, Identity-mapping. Один логический deployment рядом с billing/.
PseudonymizationЗамена prod-идентификаторов (identity_id, device_id) на subject_id, обратимая только сервисом identity-mapping. Внешним слоям Pulse доступен только subject_id.
PIIPersonally Identifiable Information. Внутри Pulse не хранится; на границе LLM Gateway фильтруется.
Purpose limitationПринцип: данные, собранные под одну цель, не могут использоваться для другой без отдельного consent. Реализован через consent-purpose-binding в Telemetry contract.
Marketing patternФормализованный маркетинговый приём в pulse_curated.marketing_patterns: переиспользуемый шаблон правила или контентного ограничения. Источник — методика Викентьева, адаптированная под software/privacy-контекст. См. marketing-patterns.md.
Composition constraintМашинно-проверяемое ограничение на структуру шаблона (например, эффект края, эффект Миллера, cognitive_load_max). Применяется brand-guardrails verifier'ом при template-validate.
Stereotype (Ст+/Ст−)Устойчивый паттерн восприятия определённой аудиторией. Положительный (Ст+) — активируется приёмом; отрицательный (Ст−) — корректируется альтернативным фреймом. Терминология из методики Викентьева.
ArchetypeSoft preference signal в Semantic Layer: культурно-поведенческий профиль subject'а (fighter, pragmatist, observer, evangelist, refugee). Используется только для ranking patterns / template variants / onboarding flows и cohort analytics; никогда не входит в applies_when правил и не используется в billing-decisions. Source: self-reported survey. Sensitive: требует consent.personalization.

3. Принципы и инварианты

Принципы, нарушение которых считается архитектурной ошибкой Pulse и блокером merge.

3.1 Privacy invariants

  1. No plaintext E2E content: Pulse никогда не видит расшифрованного содержимого vault'ов, файлов, сообщений. Только метаданные событий (типы, счётчики, агрегаты).
  2. Consent-first ingest: событие, не покрытое валидным consent'ом subject'а, не попадает во внутренние слои Pulse. Либо отбрасывается на клиенте, либо отбрасывается на boundary, в любом случае не сохраняется.
  3. Pseudonymous by default: ни один внутренний слой Pulse, кроме identity-mapping service, не видит реальных идентификаторов Kontinuum. Только subject_id.
  4. Right to erasure: при отзыве consent или удалении аккаунта все события и derived features соответствующего subject'а удаляются из feature store. Audit log сохраняет только псевдоним и хеш-метаданные.
  5. No raw data to external LLM: текст, отправляемый LLM-провайдеру, не содержит идентификаторов, сырых событий и любых полей, помеченных как PII в Telemetry contract.

3.2 Decisioning invariants

  1. Three-engine separation: каждое решение классифицируется по доминирующему движку (rule / ML / LLM). Гибридные решения декомпозируются на цепочку с явным указанием вклада каждого. Не допускается «чёрный ящик из всех трёх».
  2. Symbolic constraints dominate ML: rule engine задаёт пространство допустимых действий, ML оптимизирует внутри этого пространства. ML не имеет права предлагать действие, запрещённое правилами.
  3. LLM not in critical path: LLM может генерировать варианты, объяснения, запросы — но решения, влияющие на биллинг, capacity, compliance, проходят через symbolic verifier до исполнения.
  4. Determinism of replay: для любого decision_id из audit log при наличии артефактов (контракт по hash, версия модели, версия правил) можно воспроизвести входы и выходы решения.

3.3 Contract invariants

  1. Contract is single source of truth: типы событий и действий нигде не описаны кроме как в Telemetry/Action contract. Все runtime'ы (Rust ingest, TS Directus integration, SQL feature store, LLM tool catalog) либо генерируются из контракта, либо валидируются против него.
  2. Content-addressed versions: версия контракта = его blake3-хеш. Строковый идентификатор v0.4 — только для коммуникации между людьми.
  3. Signed updates only: переход на новую версию контракта в production-канале возможен только через signed pointer с порогом подписей (см. §11).

3.4 Audit invariants

  1. Every decision logged: ни одно действие Pulse не покидает подсистему без записи в audit log.
  2. Append-only: audit log не поддерживает UPDATE/DELETE. Коррекции делаются compensating-записями.
  3. Merkle-chained: записи audit log образуют Merkle-цепочку. Корни периодически публикуются в DHT (§11.4), обеспечивая внешне-проверяемую неизменность истории.

3.5 Operational invariants

  1. Pulse failure does not block products: отказ Pulse-core (ingest недоступен, ML не отвечает, LLM gateway упал) не должен блокировать продуктовый user flow. Деградация: события буферизуются на клиенте, действия идут по дефолтным правилам.
  2. Offline clients work: клиент применяет UI-уровневые правила и эксперимент-assignment локально, без обращения к central. Telemetry буферизуется и отправляется по восстановлении сети.

3.6 Architectural intent: privacy-by-design vs privacy-as-checkbox

Защиты в Pulse (privacy invariants, archetype-billing block, composition constraints, threat-model entries) не являются compliance overhead. Это архитектурные свойства продукта, а не overlay над функциональностью.

Это принцип верхнего уровня, влияющий на любое будущее проектное решение в Pulse.

Privacy-by-design (наш подход):

  • Защита — это invariant, который держится compile-time / structurally / architecturally.
  • Compliance с конкретными законами (GDPR, EU AI Act, CCPA, ...) — это частный случай, который автоматически удовлетворяется как побочный эффект архитектурного решения.
  • При появлении новой регуляции — добавляется тонкий layer labels / consent purposes / regional metadata. Архитектура не меняется.

Privacy-as-checkbox (мы избегаем):

  • Защита — это runtime conditional: «if subject.country ∈ EU → block X».
  • Compliance с законом — причина существования защиты; без закона защита не нужна.
  • При появлении новой регуляции — каждый раз новые runtime checks, разрастание условной логики.

Примерное соотношение мотиваций защит Pulse:

Уровень обоснованияЧто требуетДоля Pulse-защит, оправданных только этим уровнем
ЮридическийКонкретные законы и регуляции~20%
Product-fitПрирода privacy-VPN продукта~50%
СтратегическийДолгосрочное доверие, репутация, конкурентная позиция~30%

Это значит: если завтра все законы исчезнут, ~80% защит остаются как core feature; если завтра появятся новые жёсткие законы — добавляется ~20% надстройки без перестройки архитектуры.

Контр-пример (privacy-as-checkbox дизайн psychographic targeting):

compliance-only подход:
  if subject.country ∈ EU_AI_ACT_JURISDICTIONS:
    block psychographic-based pricing decisions
  else:
    allowed

Это юридически достаточно для EU, но:

  • US/LATAM subjects получают discrimination — допустимо легально, но разрушает trust;
  • маркетолог может «включить» функциональность в регионах без regulation;
  • при расширении на новые рынки — повторный legal review каждый раз;
  • ослабляется при любом давлении «давайте сэкономим — это же только EU».

Наш дизайн (privacy-by-design):

∀ subject, ∀ rule:
  rule.template_variants[*].archetype_affinity ≠ "any"
  ∧ action.audit_class ∈ {billing_*}
  → rule rejected at compile time

И дополнительно:
  archetype dimension классифицирован как `category: soft_preference`,
  не condition. Используется ТОЛЬКО для ranking variants / patterns / flows.
  Hard conditions в applies_when на archetype физически невозможны
  на уровне DSL grammar.

Это архитектурный invariant. Не зависит от country. Не обходится изнутри без collusion. Является частью того, что значит Kontinuum. См. §9.2.6 для полного описания archetype как soft preference signal.

Правило для будущих решений: при проектировании любой новой защиты в Pulse проверять обоснование по всем трём уровням (юридическому, product-fit, стратегическому). Если защита оправдана только compliance — это red flag: либо защита нужна шире, либо она overengineering. Architectural defense должна быть обоснована минимум двумя уровнями, желательно тремя.


4. Архитектурный обзор

4.1 Слои подсистемы

Pulse состоит из пяти основных слоёв (вертикальная growth-петля) и трёх горизонтальных слоёв (cross-cutting concerns), пронизывающих все основные.

                Source: App / Veil / Tether / Send / Node / Billing


        ╔══════════════════════════════════════════════════════╗
   1.   ║  Event / Usage Ingest + Feature Store                ║
        ╠══════════════════════════════════════════════════════╣
   2.   ║  ML Layer (churn, uplift, intensity, affinity)       ║
        ╠══════════════════════════════════════════════════════╣
   3.   ║  Policy DSL (rule engine, eligibility, frequency)    ║
        ╠══════════════════════════════════════════════════════╣
   4.   ║  Experiment Layer (assignment, bandit, statistics)   ║
        ╠══════════════════════════════════════════════════════╣
   5.   ║  LLM Interface (NL, copy generation, explanations)   ║
        ╚══════════════════════════════════════════════════════╝


                Sink: App / Billing / Node action endpoints

       ┌───────────────────────────────┼───────────────────────────────┐
       │                               │                               │
       ▼                               ▼                               ▼
  ┌────────────┐                ┌─────────────┐                ┌──────────────┐
  │ Identity & │                │  Semantic   │                │  Audit Log   │
  │  Consent   │                │   Layer     │                │  (Merkle)    │
  └────────────┘                └─────────────┘                └──────────────┘
       ▲                               ▲                               ▲
       │     горизонтальные слои применяются на каждом этапе          │
       └───────────────────────────────────────────────────────────────┘

4.2 Внешние границы

Pulse имеет ровно три типа внешних границ:

ГраницаНаправлениеКонтрактГарантии
Telemetry inпродукт → PulseTelemetry contractevents типизированы, consent-проверены до ingest, pseudonymous
Action outPulse → продуктAction contractидемпотентные, валидированные, с decision_id для traceability
Human / LLM I/Oмаркетолог ↔ PulseNL query + tool catalog (производный от Action contract)PII-фильтрация, audit-logging, без сырых данных

Никаких других путей данных in/out не существует. В частности: Pulse не имеет прямого доступа к Directus БД или vault-данным. Всё через telemetry-emit и action-call.

4.3 Контракт как ось архитектуры

Главное архитектурное решение: Pulse параметризуется контрактами как декларативной конфигурацией, а не зашивает структуры данных в код. Изменение контракта проходит через codegen и/или runtime-reload (§5), и все слои Pulse автоматически перенастраиваются.

Это даёт три ключевых свойства:

  • Один источник истины для типов на нескольких языках (Rust, TS, SQL, JSON Schema).
  • Federation-узлы криптографически согласовывают активную версию контракта (§11).
  • Эволюция growth-логики не требует пересборки и редеплоя ядра — только publication новой версии контракта.

4.4 Минимальная топология

┌──────────────────────────────────────────────────────────────────────┐
│                          CLIENT (App/Veil/Tether/Send)               │
│  ┌────────────┐  ┌────────────────┐  ┌────────────────────────────┐  │
│  │ Local DSL  │  │ ML inference   │  │ Local event buffer + emit  │  │
│  │ (UI rules) │  │ (lightweight)  │  │ + consent filter           │  │
│  └────────────┘  └────────────────┘  └────────────────────────────┘  │
└──────────────────────────────────┬───────────────────────────────────┘
                                   │ telemetry (pseudonymized)

┌──────────────────────────────────────────────────────────────────────┐
│                          EDGE NODE (Tier 0/1/2)                      │
│  ┌────────────────────┐  ┌──────────────────────────────────────────┐│
│  │ Federation-policy  │  │ Telemetry relay (normalize + forward)    ││
│  │ DSL (capacity etc) │  │                                          ││
│  └────────────────────┘  └──────────────────────────────────────────┘│
└──────────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────┐
│                       CENTRAL (Pulse Core)                           │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────────┐  │
│  │ Ingest     │  │ Feature    │  │ ML train/  │  │ Policy DSL     │  │
│  │ + Consent  │  │ Store      │  │ infer      │  │ (billing/promo)│  │
│  └────────────┘  └────────────┘  └────────────┘  └────────────────┘  │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────────┐  │
│  │ Experiment │  │ LLM        │  │ Identity-  │  │ Audit Master   │  │
│  │ Platform   │  │ Gateway    │  │ Mapping    │  │ (Merkle root)  │  │
│  └────────────┘  └────────────┘  └────────────┘  └────────────────┘  │
└──────────────────────────────────┬───────────────────────────────────┘


                       ┌─────────────────────────┐
                       │  EXTERNAL LLM PROVIDER  │
                       └─────────────────────────┘

Детали места выполнения каждого слоя — §10.

4.4-bis Deployment topology: single-UI стратегия

Pulse и Billing/CRM используют единый admin UI на основе Directus. Никакого отдельного desktop-приложения, никаких параллельных интерфейсов для маркетинга/billing/support. Это зафиксированное архитектурное решение, действующее с v0.1.

Что это значит

  • Один admin UI для маркетинга, billing и support — Directus admin поверх shared Postgres.
  • Стандартные коллекции Directus управляют curatable объектами Pulse: rules, templates, segments, experiments, consent purposes, approval queue, audit-views.
  • Custom interfaces (Directus extensions) добавляются по мере необходимости для специфичных полей: DSL editor, metric picker, action picker, experiment status display.
  • Custom modules (более крупные Directus extensions) реализуют сложные рабочие сценарии: Rule Studio, Experiments Console, Audit Explorer, Pulse Chat. Подключаются к тому же Directus admin как полноэкранные приложения.
  • Pulse-core (Rust) — backend без собственного UI; обслуживает API для Directus extensions и принимает решения по событиям.

Почему именно так

АльтернативаПочему отклонена
Отдельное Tauri-app для маркетингаДублирует auth, permissions, audit; ломает single-admin UX; повышает стоимость M0 без явной выгоды.
Web-admin отдельно от DirectusТо же самое + дублирует инфру и dev-стек.
Pulse-логика как Directus flows / hooks (no Rust)Не вытягивает ingest throughput, ML, SMT-валидатор, LLM Gateway; ломает privacy boundary identity-mapping.
Чистое contract-coupling без shared UIМаркетингу нужны два инструмента вместо одного; CRUD на правилах и шаблонах без причины дороже.

Решение даёт три ключевых свойства:

  1. Минимум инфраструктуры на старте — Directus уже есть, шаблоны permission/auth/audit готовы.
  2. Privacy invariant сохраняется — identity-mapping в отдельной БД, Directus admin физически не имеет к ней доступа (см. §9.1).
  3. Pulse-core свободен в выборе runtime — Rust остаётся для перформанса, MV, SMT, LLM Gateway.

Граница ответственности UI ↔ backend

┌─────────────────────────────────────────────────┐
│           Directus admin UI                     │
│  - стандартные коллекции                        │
│  - custom interfaces / fields                   │
│  - custom modules (Rule Studio, Audit, ...)     │
└──────────────┬──────────────────────────────────┘
               │ DB writes (curated, billing)
               │ + API calls к pulse-core

┌─────────────────────────────────────────────────┐
│       Shared Postgres                           │
│  schema: billing            (Directus-owned)    │
│  schema: pulse_curated      (Directus-owned)    │
│  schema: pulse_runtime      (Pulse-core-owned)  │
└──────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────┐
│  pulse-core (Rust) — без UI                     │
│  + identity-mapping-service (отдельная БД)      │
└─────────────────────────────────────────────────┘
  • Объекты в pulse_curated.* (rules, templates, experiments, consent_purposes) — редактируются через Directus admin, читаются Pulse-core при необходимости.
  • Объекты в pulse_runtime.* (events, features, decisions, audit) — пишутся только Pulse-core, читаются Directus admin через ограниченные views с RLS.
  • Объекты в identity_mapping.* — недоступны Directus admin вообще; обращение возможно только через идентифицируемое сервис-API.

Inкрементальность UI

Single-UI стратегия не означает «всё сразу делать как custom module». Каждый M-релиз production-ready, но UI растёт постепенно:

   M0:  стандартные Directus коллекции (CRUD as-is)
        DSL — обычная textarea


   M1:  custom interfaces (CodeMirror DSL editor, metric/action pickers)


   M2:  первый custom module — Rule Studio


   M3:  модули Experiments Console, Audit Explorer


   M4:  модуль Pulse Chat (LLM)

Throwaway-код в M0 (например, голая textarea для DSL с server-side валидацией) принимается осознанно: его легко заменить custom interface'ом в M1, не меняя ни backend-API, ни схему БД. Подробности по эволюции UI — см. issue #45.


5. Контрактный слой

5.1 Назначение

Контрактный слой — это единственное место, где описано, какие события могут приходить в Pulse и какие действия могут из него исходить. Все остальные компоненты подсистемы потребляют контракты либо через codegen at build time, либо через runtime-load.

Контрактов два:

  • Telemetry contract (§6) — типы и схемы событий.
  • Action contract (§7) — типы и схемы действий.

Дополнительно с контрактами идут (но это не отдельные контракты, а декларативные артефакты, привязанные к контрактам):

  • Consent purposes registry — список целей сбора данных, на которые ссылаются события.
  • Metrics registry (Semantic Layer, §9.2) — определения метрик, ссылающиеся на поля Telemetry.
  • Tool catalog (производный от Action contract) — описания инструментов для LLM.

5.2 Формат и идентификация

Каждый контракт — дерево YAML-документов, сериализуемое канонически (RFC 8785 JSON Canonicalization после YAML→JSON конверсии). Идентификатор контракта:

contract_id := blake3(canonical_serialize(contract))
              # пример: b3:7a2f...e91d

Семантическая версия (v0.4.1) ведётся параллельно как человекочитаемый идентификатор, но истиной является только hash. Два контракта с одинаковым v0.4.1 строковым тегом, но разным содержимым — это разные контракты.

5.3 Каналы и pointers

Контракт распространяется через каналы — именованные потоки активных версий:

КаналНазначение
pulse/mainProduction-канал. Используется central и большинством федерации.
pulse/betaКанал предрелизных версий. Используется частью узлов для проверки совместимости.
pulse/stagingКанал для внутренних тестов команды.
pulse/<x>Кастомные каналы для federation-сегментов (например, EU-only consent-purposes).

Каждый канал в любой момент времени имеет signed pointer:

yaml
channel: "pulse/main"
current_contract: "b3:7a2f...e91d"
previous_contract: "b3:5c1e...b04a"
effective_from: 2026-06-01T00:00:00Z
expires: null
signatures:
  - signer: "team-pulse-multisig"
    sig: "ed25519:8f3a..."
  - signer: "team-platform"
    sig: "ed25519:2c91..."
threshold: 2_of_3
note: "Add `grant_relay_credits` action; non-breaking for telemetry."

Pointer публикуется в Kontinuum DHT по ключу pulse-channel:{channel_name}, см. §11.

5.4 Lifecycle контракта

   draft  ─────►  proposed  ─────►  staging  ─────►  active  ─────►  superseded
     │                                │                                 │
     │ локальная разработка           │ внутренняя проверка             │ заменён более новым
     │ ещё не публикуется в канале   │ deploy в pulse/staging          │ pointer указывает на следующий
     │                                │                                 │
     └────────────────────────────────┴─────────────────────────────────┘

active контракт может оставаться доступен для read'а даже после superseded — для воспроизведения исторических решений из audit log.

5.5 Совместимость и эволюция

Изменения контракта классифицируются:

КлассПримерДопустимо в pulse/main?
additiveДобавлено новое опциональное поле, новый event-тип, новое actionДа, без миграции
enrichmentСуществующее опциональное поле помечено requiredТолько с миграцией пишущих сторон
renameПереименование поля или типаТолько с алиасами на n версий
semanticИзменён смысл поля при том же имениЗапрещено: переименовать обязательно
breakingУдалено поле, ужесточён тип, изменена обязательностьТолько с явным multi-version live

Контракт-валидатор (CI-step при publishing) проверяет класс изменения автоматически и блокирует publish, если класс несовместим с каналом.

5.6 Compilation pipeline

       contracts/*.yaml


   ┌──────────────────────┐
   │  Contract Compiler   │
   │  (kontinuum-pulse/   │
   │   tools/cc)          │
   └──────────────────────┘

   ┌──────────┼──────────┬─────────────┬──────────────┬─────────────┐
   ▼          ▼          ▼             ▼              ▼             ▼
 Rust       TS         SQL           JSON          OpenAPI       LLM tool
 types     types     schema +       Schema       (Directus)      catalog
 (ingest, (client,   migrations    (runtime                      (gateway)
 DSL,     billing,  (feature        validation)
 DSL)     LLM       store)
          tool)

Compiler — это отдельная утилита внутри kontinuum-pulse/tools/cc/ (Rust). Запускается:

  • В build.rs Rust-крейтов Pulse — для compile-time типов.
  • В Nx-таргете pulse-contracts:gen — для генерации TS/SQL артефактов в монорепо.
  • В CI при publishing новой версии — для валидации и генерации distribution-bundle.

5.7 Runtime modes

Pulse поддерживает три режима использования контракта в рантайме:

  1. Compile-time only (default для central core): бинарь Pulse-core собран против фиксированного contract_id. Изменение контракта = пересборка + редеплой. Преимущество: zero runtime overhead.
  2. Runtime-load: бинарь загружает контракт на старте по contract_id из конфигурации или DHT-канала. Используется для клиентов и edge-нод, чтобы они могли получать новые правила без обновления приложения.
  3. Hot-reload (опционально, milestone M3): бинарь следит за изменением pointer'а канала и атомарно переключается на новую версию контракта. В пределах switching window поддерживается dual-mode (in-flight операции на старой, новые — на новой). Доступно только в central, не в клиентах.

6. Telemetry contract

6.1 Назначение

Telemetry contract описывает исчерпывающий набор типов событий, которые могут попасть в Pulse. Событие, тип которого не описан в активном контракте, отбрасывается на boundary — даже если эмиттер был обновлён раньше.

6.2 Структура

Контракт — корневой документ со списком категорий событий. Каждая категория содержит набор event-типов.

yaml
contract: telemetry
version: 0.4.0          # human-readable
purposes_required:       # ссылка на consent-purposes registry
  - analytics.usage
  - personalization
  - experiments

categories:
  veil:
    description: "События VPN-подсистемы Kontinuum Veil"
    events:
      session_started:
        purpose: analytics.usage
        fields:
          subject_id:    { type: subject, pii: false, required: true }
          session_id:    { type: ulid,    pii: false, required: true }
          relay_node:    { type: node_id, pii: false, required: true }
          client_geo:    { type: country_code, pii: false, required: false }
          started_at:    { type: timestamp, pii: false, required: true }
        retention_days: 90
        derived_features: [veil.session_count_7d, veil.relay_node_affinity]

      session_ended:
        purpose: analytics.usage
        fields:
          subject_id:      { type: subject,   pii: false, required: true }
          session_id:      { type: ulid,      pii: false, required: true }
          duration_seconds:{ type: u32,       pii: false, required: true }
          bytes_in:        { type: u64,       pii: false, required: true }
          bytes_out:       { type: u64,       pii: false, required: true }
          status:          { type: enum[success, error, timeout], pii: false, required: true }
        retention_days: 90

  tether:
    description: "События удалённого Android-управления"
    events:
      session_started:
        ...
      sms_received_aggregated:
        purpose: analytics.usage
        fields:
          subject_id:    { type: subject, pii: false, required: true }
          count:         { type: u32, pii: false, required: true }
          window_minutes:{ type: u16, pii: false, required: true }
        notes: "Только агрегаты. Никаких текстов SMS никогда."
        retention_days: 30

  app:
    description: "События основного приложения"
    events:
      space_opened: ...
      pairing_completed: ...

  billing:
    description: "Биллинговые события из Directus"
    events:
      subscription_started: ...
      payment_failed: ...
      plan_changed: ...

  pulse_internal:
    description: "Само-телеметрия Pulse для дебага"
    purpose: ops
    events:
      decision_made: ...
      experiment_assigned: ...

6.3 Поля события

Базовый набор обязательных полей в каждом событии:

ПолеТипОписание
subject_idsubjectПсевдонимизированный идентификатор. Всегда первое поле.
event_typestring{category}.{name}, например veil.session_started.
schema_hashbytes32Hash контракта, под которым emit-сторона валидирует событие.
emitted_attimestampUTC время эмита на источнике.
ingested_attimestampЗаполняется ingest-слоем Pulse. Не доверяется источнику.
sourceenumclient / edge / central / billing — где было сгенерировано.
consent_tokenbytesПодписанный token, подтверждающий действующее consent на момент эмита.

Доменные поля — в fields: секции event-типа.

6.4 PII-классификация

Каждое поле в контракте обязано иметь явный флаг pii: bool. Поля с pii: true:

  • Хранятся в central отдельно, с шифрованием at rest и более строгой ACL.
  • Никогда не передаются в LLM Gateway.
  • Не попадают в feature store; вместо них в feature store идут производные (например, хеш или категория).
  • При right-to-erasure удаляются first.

Каждое событие в контракте обязано декларировать purpose — одну из целей в Consent purposes registry. Ingest-слой Pulse при приёме события:

  1. Извлекает subject_id и purpose.
  2. Запрашивает Consent Store: разрешён ли этот purpose для этого subject'а на момент emitted_at.
  3. Если нет — событие отбрасывается до записи в feature store. Может быть оставлен счётчик «отброшено по consent» без идентификатора.

6.6 Retention

Каждое событие имеет retention_days. По истечении срока запись удаляется из feature store. Audit log для решений, использовавших это событие, сохраняет хеш входа, не само значение — это позволяет проверить, что решение было консистентным, не раскрывая истёкших данных.

6.7 Derived features

Поле derived_features в контракте — это ссылка на Metrics registry (Semantic Layer, §9.2). При добавлении нового события можно сразу декларировать, какие фичи из него производятся; Semantic Layer обязан содержать их определения.

6.8 Канонический примитивный набор типов

В контракте используются только из этого набора:

primitive:    bool | u8/u16/u32/u64 | i8/i16/i32/i64 | f32/f64 | string
identity:     subject | node_id | space_id | ulid
temporal:     timestamp | duration_seconds | duration_ms
spatial:      country_code | geo_region (ISO 3166-1 / opaque)
enum[...]:    declared inline
bytes(N):     fixed-length bytes
container:    list<T> | map<K,V> | optional<T>

Произвольные структуры не допускаются. Если нужна структурированная агрегация — отдельное event-type.

6.9 Sampling

Для high-volume событий (типа app.feature_used, pulse_internal.rule_evaluated) ingest и хранилище могут не справиться с emit'ом каждой попытки. Контракт предоставляет per-event-type client-side sampling.

6.9.1 Декларация

В meta-schema добавляется optional field sample_rate (0.0–1.0). Default = 1.0 (full sampling, без потерь). При sample_rate: 0.1 клиент эмитит примерно каждое 10-е событие.

yaml
feature_used:
  purpose: analytics.usage
  retention_days: 180
  sample_rate: 0.1                   # 10% sample by default
  sample_rate_override_allowed: true # ops может временно поднять до 1.0 при debug
  fields: { ... }

6.9.2 Алгоритм sampling на клиенте

Чтобы выбор был детерминированным per (subject, event_type), не случайным per-call (иначе sticky cohort не сработает):

sampling_seed = blake3(subject_id || event_type)[0..8]   # u64
threshold = u64::MAX * sample_rate
emit_decision = (sampling_seed[0..8] as u64) < threshold  # эмитим, если попали ниже порога

Это даёт stable per-subject sampling: один и тот же юзер всегда либо в sample, либо нет (внутри одной generation). Cohort analytics остаются consistent.

Альтернатива — per-event random — используется когда нужна полная независимость (e.g. error reports, rate-limit observability): sample_strategy: random в meta-schema 1.1+. В M0 контракте применён к app.error_occurred и pulse_internal.rate_limit_hit.

Scope rule. sample_strategy: random допустим только для events с purpose IN (ops, meta) — operational и self-телеметрия, где per-subject consistency не требуется и важна несмещённая population-level статистика. Для events с purpose IN (analytics.usage, personalization, experiments, marketing.*) применяется только sticky — иначе cohort analytics и retention will diverge across subject buckets. Validate.py enforce'ит это правило (M1+).

6.9.3 Компенсация в derivation

Metric derivation в semantic layer должен умножать на 1 / sample_rate для агрегатов:

yaml
feature_used_count:
  source_events: [app.feature_used]
  derivation: "COUNT(*) * (1.0 / app.feature_used.sample_rate)"

validate.py enforce'ит: каждая metric, ссылающаяся на event с sample_rate < 1.0, либо имеет explicit compensation в derivation, либо явно помечена sampled: true (тогда значение трактуется как «sample-only, без оценки population»).

6.9.4 Audit trail

Ingest при приёме события записывает effective sample_rate в events.fields._sample_rate (если override был активен). Это позволяет replay tooling вычислить корректные historical aggregations при изменении rate.

6.9.5 Privacy

Sampling не уменьшает privacy gradient — те юзеры, что попали в sample, всё ещё имеют события в БД. Это не differential privacy. Для DP-budget — отдельный механизм M5+.


7. Action contract

7.1 Назначение

Action contract описывает исчерпывающий набор действий, которые Pulse может предложить продуктовым системам. Действие, не описанное в активном контракте, не может быть инициировано через Pulse — даже из LLM, даже из вручную написанного правила.

7.2 Структура

yaml
contract: action
version: 0.4.0

actions:
  send_promo_email:
    target: app
    description: "Отправить promo-письмо subject'у через App-уровень"
    parameters:
      subject_id:   { type: subject, required: true }
      template_id:  { type: string, required: true }
      params:       { type: map<string, string>, required: false }
      send_after:   { type: timestamp, required: false }
    side_effect: outbound_message
    idempotency_key: "{subject_id}:{template_id}:{send_after | now_day}"
    requires_consent: marketing.email
    rate_limit:
      per_subject: { count: 1, window: "24h" }
      per_template: { count: 1, window: "7d", per: subject_id }
    audit_class: outbound_communication

  grant_relay_credits:
    target: billing
    description: "Зачислить relay-credits на subject'а"
    parameters:
      subject_id: { type: subject, required: true }
      amount_gb:  { type: u32, required: true, max: 100 }
      reason:     { type: string, required: true, max_length: 200 }
      expires_at: { type: timestamp, required: false }
    side_effect: billing_state_change
    idempotency_key: "{subject_id}:{reason}:{day(now)}"
    requires_role: pulse-promo-issuer
    audit_class: billing_credit

  set_pricing_tier:
    target: billing
    parameters:
      subject_id:    { type: subject, required: true }
      tier:          { type: enum[free, standard, pro, pro_plus], required: true }
      reason:        { type: string, required: true }
      effective_from:{ type: timestamp, required: false }
    side_effect: billing_state_change
    audit_class: tier_change
    requires_human_approval: true

  show_ui_banner:
    target: app
    parameters:
      subject_id: { type: subject, required: true }
      banner_id:  { type: string, required: true }
      params:     { type: map<string, string>, required: false }
      expires_at: { type: timestamp, required: false }
    side_effect: ui_state
    idempotency_key: "{subject_id}:{banner_id}"
    requires_consent: personalization
    audit_class: ui_personalization

7.3 Обязательные атрибуты

АтрибутОписание
targetКуда направляется действие: app, billing, node, external_provider.
descriptionЧеловекочитаемое описание. Используется LLM Gateway в tool catalog'е.
parametersТипизированная схема параметров.
side_effectКласс эффекта: outbound_message, billing_state_change, ui_state, none (read-only).
idempotency_keyTemplate, по которому action target проверяет повторы.
requires_consentКакой consent-purpose обязателен для применения.
requires_roleКакая роль вызывающего нужна (для human/billing-acting).
rate_limitОграничения по частоте; проверяются action target'ом и pre-flight'ом в Pulse.
requires_human_approvalЕсли true, action не выполняется автоматически, попадает в approval queue.
audit_classКатегория для audit log, влияет на retention и уровень доступа к записи.

7.4 Поток исполнения

   Pulse Decision           Action Contract             Action Target
        │                        │                            │
        │ build action call      │                            │
        │───────────────────────►│                            │
        │                        │ validate schema            │
        │                        │ check consent              │
        │                        │ check rate limit           │
        │                        │ check approval queue       │
        │                        │ assign decision_id         │
        │                        │ append to audit log        │
        │                        │───────────────────────────►│
        │                        │                            │ execute idempotently
        │                        │                            │ confirm
        │                        │◄───────────────────────────│
        │ ack / nack             │                            │
        │◄───────────────────────│                            │

Между Pulse и target'ом обязательная двухсторонняя ack-семантика: target подтверждает применение действия или отклоняет с причиной (например, consent_revoked_meanwhile, idempotency_replay, target_unavailable). Audit log хранит обе записи (request + ack/nack).

7.5 Категории side_effect

side_effectГарантииАудит
noneRead-only, не меняет состояниеМинимум: subject + decision_id
ui_stateМеняет UI-отображение у конкретного клиента+ параметры баннера
outbound_messageОтправляется внешнее сообщение (email/push/in-app)+ получатель, шаблон, время
billing_state_changeМеняет биллинговое состояние (тариф, баланс, capacity)+ полный delta, requires_role
external_provider_callВызов внешнего API (платёжного провайдера, LLM и т.д.)+ контракт вызова, fingerprint

7.6 Tool catalog для LLM

Из Action contract автоматически генерируется JSON-каталог инструментов для LLM Gateway. Каталог содержит только actions с target: app и side_effect: ∈ {none, ui_state} по умолчанию; actions более высоких классов требуют явного permission в LLM-сессии. LLM физически не может вызвать action, отсутствующий в её сессионном каталоге — это enforced на уровне gateway, не trust-based.

7.7 Версионирование действий

Деактивация action'а — это breaking change для всех правил и моделей, использующих этот action. Контракт-валидатор отслеживает зависимости (через rules registry и references в DSL) и блокирует deactivation, пока зависимости не сняты. Альтернатива: помечать action deprecated: true, оставляя для legacy-правил с warnings в audit log.

7.8 Template engine

Все content-producing actions (show_ui_banner, send_transactional_email, send_marketing_email, future send_push, show_in_app_inbox_message) рендерят содержимое из templates.content (JSONB в pulse_curated.templates) с подстановкой template_params. M0 фиксирует один engine: Handlebars (через crate handlebars ≥ 5.0).

Почему Handlebars, а не Jinja2 / Liquid:

  • Logic-less philosophy — minimal встроенная логика снижает поверхность атаки template injection и облегчает review маркетингом и legal.
  • Нативная поддержка {{variable}}, {{#if}}, {{#unless}}, {{#each}} — достаточно для всего M0-M2 спектра шаблонов.
  • Существующий Rust crate с активным maintenance и hard limits на recursion depth.
  • Хорошо известен дизайнерам и маркетологам (Mustache/Handlebars-семейство — индустриальный стандарт для transactional email).

Sandbox (обязателен на pulse-core):

rust
let mut hbs = Handlebars::new();
hbs.set_strict_mode(true);                 // unknown var → render error, не silent ""
hbs.set_max_recursion_depth(8);            // защита от bomb
hbs.set_dev_mode(false);
// Запрещённые helpers (НЕ зарегистрированы):
//   exec, partial-inline (`{{> @partial-block}}`), helper из user-controlled string.
// Разрешены: if, unless, each (с bounds-check), with, lookup, log (disabled in prod).

Доступные переменные в context:

Корневой ключТипИсточник
paramsobjectAction parameter template_params (map<string,string> из action contract)
subjectobjectWhitelist subject dimensions, заявленных в template's requires_subject_fields (см. ниже)
i18nobjectЛокализованные строки из templates.content.i18n[{locale}]
nowtimestampДата рендера (UTC); удобно для «Срок действия истекает {{format_date now '+7d'}}»

subject.* namespace — двухуровневый whitelist:

  • Action-level (ceiling): requires_subject_fields в action contract задаёт абсолютный максимум полей subject.*, доступных любому шаблону этого action_type. Меняется только при изменении action contract (требует review + content-hash bump).
  • Template-level (narrower subset): templates.content.requires_subject_fields для конкретного шаблона. Validator (brand_guardrails class A) при template upload в Directus enforce'ит: (1) template-level action-level (нельзя расширить whitelist шаблоном); (2) каждая использованная в content переменная {{subject.X}} присутствует в template-level whitelist. Это закрывает риск утечки несвязанных subject-атрибутов через template substitution (см. §12.2 T11).

Локализация. Один template = один record в pulse_curated.templates. Контент — JSONB { i18n: { en: {...}, ru: {...}, pt-BR: {...} }, default_locale: "en" }. Render выбирает локаль по action.parameters.locale или, если не задано, по subject.preferences.locale, fallback — default_locale. Plurals для M0 — статические per-locale форма (one/other); ICU plural rules — M2+.

Контракт-уровневая проверка. action-contract-schema.yaml НЕ описывает template syntax (это runtime concern), но при template upload в Directus:

  1. Парсер Handlebars проверяет syntax валидность.
  2. Brand-guardrails class A (см. §8.5.4) валидирует обязательные элементы (unsubscribe link для marketing email, sender address, etc.).
  3. Все использованные переменные сравниваются с requires_subject_fields + action.parameters — несовпадение блокирует save.

Test corpus. В contracts/test-corpus/templates/ лежат: golden template per action_type + invalid examples (forbidden helper, undeclared subject field, missing localized variant). Используется в snapshot-tests render'а.

7.9 Action error handling и retry policy

Глобальные конвенции для всех action.dispatch failure modes. Per-action overrides — через field retry_policy в action contract (см. action-contract-schema.yaml).

7.9.1 Lifecycle dispatch'а

decision → pulse-core dispatcher → target

                ┌─────────────────────┼─────────────────────┐
                ▼                     ▼                     ▼
       immediate ack            async webhook         no ack required
     (target == app)         (target == external_     (target == pulse,
                              provider, billing)        side_effect: none)
  • Immediate ack: target subsystem отвечает синхронно в течение action.dispatch_timeout (M0 default 5 s).
  • Async webhook: target подтверждает delivery позже (например, SendGrid webhook → pulse_runtime.outbound_events). Промежуточный action_acks.status = deferred.
  • No ack: записывается decision + audit_log без ожидания confirmation.

7.9.2 Retry policy schema

Структура (необязательная, default — null = no retries):

yaml
retry_policy:
  max_attempts: 3                # включая первый. 1 = no retry.
  backoff:
    initial_ms: 1000             # base delay
    multiplier: 2.0              # exponential factor
    max_ms: 60000                # ceiling
    jitter_pct: 25               # ±25% randomization
  retry_on:                       # классы ошибок, которые retry-абельны
    - network_error
    - target_unavailable
    - rate_limited                # honor Retry-After header
    - provider_5xx
  abort_on:                       # явный stop list (не retry)
    - schema_invalid
    - consent_missing
    - permanent_failure           # provider 4xx (kроме 429)
  dead_letter:
    on_exhaustion: audit_only     # audit_only | requeue_manual | escalate_to_human

7.9.3 Success criteria per side_effect

side_effectsuccessfailure
nonerow создан в decisions + audit_logDB error (retry by orchestrator, не action)
ui_stateapp ack'нул (POST /actions/ack) ≤ 60 stimeout, app ack reject (e.g. banner_id collision)
outbound_messageprovider 2xx И delivery webhook ≤ 24 hprovider 4xx (не 429); delivery webhook = bounced
billing_state_changebilling service ack + idempotency check passedbilling reject; idempotency conflict
external_provider_callprovider 2xx (если webhook есть — wait for ack)provider 5xx до retries exhaustion

action_acks.status:

  • success — все критерии выполнены.
  • deferred — initial ack OK, ждём webhook.
  • rejected — terminal failure от target (4xx, schema reject, idempotency conflict).
  • failed — exhausted retries / timeout.

7.9.4 Dead-letter handling

При dead_letter.on_exhaustion:

  • audit_only (default M0) — пишет audit_log(audit_class=outbound_communication, operation=dispatch_failed, payload={decision_id, attempts, last_error}) и stop. Никаких alerts.
  • requeue_manual — попадает в pulse_curated.approval_queue(kind='dead_letter_requeue'); ops-оператор может решить retry / drop.
  • escalate_to_human — то же + PagerDuty alert через AlertManager (M1+; для M0 — manual review).

7.9.5 Idempotency и timezone

idempotency_key action'а — template-строка с placeholder'ами ({subject_id}, {template_id}, {send_day} и т.д.). Resolver:

  • Placeholders из base fields → implicit (subject_id, decision_id, evaluated_at).
  • Placeholders из action.parameters → значения текущего dispatch'а.
  • Time-bucket placeholders:
    • {now_minute} / {now_hour} / {now_day} — UTC bucket from decided_at.
    • {send_day} — для outbound_message actions: если subject имеет dimension subject.timezone, используется local day; иначе UTC day.

Это гарантирует что юзер в Tokyo не получит promotional email "в 16:00" просто потому что UTC day boundary падает на середину их дня.

Fallback при subject.timezone = NULL (subject ни разу не эмитил app.startup с timezone полем):

  1. {send_day} resolved как UTC day (date_trunc('day', decided_at AT TIME ZONE 'UTC')).
  2. В соответствующую pulse_runtime.decisions row пишется action_parameters.tz_fallback: true (informational; не часть action contract).
  3. Маркетолог при создании rule может явно установить parameters.send_after с фиксированным UTC offset, чтобы избежать неподходящих локальных часов для anonymous-timezone subjects.
  4. pulse_internal.rate_limit_hit НЕ эмитится для этого fallback — это нормальная ветка, не error path.

7.9.6 Replay семантика

При decision replay (для testing / audit recovery):

  • Action dispatch не повторяется, даже если original dispatch завершился failed.
  • Replay генерирует только новую decision row с replay_of: <original_decision_id> и shadow action params (как log_only).
  • Это гарантирует idempotency на уровне reality, не только DB.

8. Pipeline: 5 слоёв

8.1 Event / Usage Ingest + Feature Store

8.1.1 Назначение

Принимать события из источников (App, Veil, Tether, Send, Node, Billing), фильтровать по consent, нормализовать, помещать в Feature Store. Поддерживать читалку фичей по subject и временному окну.

8.1.2 Ingest pipeline

   Source emit


   ┌──────────────────────────────────────────────┐
   │  Client-side / source-side filter            │
   │  - consent check (locally cached)            │
   │  - schema validate (contract by hash)        │
   │  - PII strip (если поле классифицировано)    │
   └────────────────────┬─────────────────────────┘
                        │ batched, signed

   ┌──────────────────────────────────────────────┐
   │  Edge Node relay (если применимо)            │
   │  - rate-limiting per-source                  │
   │  - drop malformed                            │
   │  - forward to central                        │
   └────────────────────┬─────────────────────────┘


   ┌──────────────────────────────────────────────┐
   │  Central ingest                              │
   │  1. Verify consent token (re-check)          │
   │  2. Validate schema against active contract  │
   │  3. Pseudonymize identity → subject_id       │
   │  4. Normalize timestamps to UTC              │
   │  5. Apply derived-feature computations       │
   │  6. Write to Feature Store                   │
   │  7. Emit pulse_internal.event_ingested       │
   └──────────────────────────────────────────────┘

8.1.3 Feature Store

Структура:

ХранилищеНазначение
Online storeТекущее значение фичей по subject. Используется для real-time decisioning. PostgreSQL + кэш в памяти на старте, Redis позже.
Offline storeИсторические фичи + сырые normalized events. Используется для тренировки ML, batch-аналитики. PostgreSQL на старте, ClickHouse при росте.
Streaming viewMaterialized view с прокаткой агрегатов окнами (session_count_7d, bytes_avg_30d). Обновляется по приходу событий.

Каждая фича в Online/Offline store ссылается на:

  • subject_id
  • feature_name (из Metrics Registry)
  • metric_version (версия определения в Semantic Layer)
  • value (типизированное)
  • computed_at
  • source_events_hash (опционально, для reproducibility)

8.1.4 On-device aggregation (опциональный режим)

Для subject'ов без consent на analytics.usage, но с consent на personalization, клиент:

  1. Хранит сырые события локально, не отправляя их в central.
  2. По расписанию вычисляет локальные фичи (на тех же определениях из Semantic Layer).
  3. Отправляет в central только агрегаты с локальной differential privacy (Laplace noise scaled to ε ≤ 1.0).

В Feature Store такие фичи маркируются как noisy=true и используются ML и DSL с поправкой на шум. См. также §10.

8.1.5 Backfill

Добавление новой derived-фичи в Semantic Layer запускает backfill:

  • Для событий, ещё не вышедших за retention_days — пересчёт по сырым событиям.
  • Для исторических событий, чьё retention_days истёк — backfill невозможен; фича доступна только с available_from метки.

8.2 ML Layer

8.2.1 Назначение

Предоставлять статистические скоры по subject'у для использования в DSL и LLM объяснениях. Pulse намеренно держит ML тонким — это не ML-платформа, а небольшой набор моделей с чёткими target-функциями.

8.2.2 Базовый набор моделей (v0.1)

МодельTargetАлгоритм по умолчанию
churn_prob_7dP(subject inactive on day 7 from now)LightGBM
churn_prob_30dP(subject churned in 30 days)LightGBM
intensity_scoreZ-score текущей активности vs baseline subject'аRobust z-score (median)
product_affinityPer-product propensity score (Veil/Tether/Send/...)Multi-output GBM
uplift_per_actionΔ(reward) от применения action vs notCausal forest / X-learner
next_best_actionargmax(uplift × value − cost), subject to rules(composite, см. §8.3)

Все модели тренируются на consent-protected данных feature store. Tренировочный пайплайн — отдельный сервис (pulse-trainer), запускаемый по расписанию (cron / Nx target).

8.2.3 Inference modes

  • Central inference (default): модель загружена в Pulse-core, скоринг при запросе из DSL. Латентность <50ms на feature row.
  • On-device inference (selective): лёгкие модели (churn_prob, intensity) могут поставляться клиенту как ONNX-артефакты и считаться локально. Используется для subject'ов без consent на отправку сырых данных. Артефакт распространяется через тот же signed-channel механизм, что и контракты.
  • Edge inference: не используется в v0.1. Возможный milestone M3 для federation-сегментов с высокими privacy-требованиями.

8.2.4 Калибровка и мониторинг

  • Каждая модель имеет calibration metric (Brier score, ECE) поверх holdout-сета.
  • Output модели всегда сопровождается confidence band: not just point estimate.
  • Дрейф детектируется по разнице распределения признаков между training-window и live-window. Срабатывание = автоматический re-train.

8.2.5 Запрет: ML не пишет в action contract напрямую

ML возвращает score / ranking / candidate-set. Финальное решение о действии принимается DSL (§8.3) с учётом ML-output как одного из входов. Прямой путь «ML → action» запрещён архитектурно: action emit-функция требует decision-context, который собирается только в DSL.

8.3 Policy DSL

8.3.1 Назначение

Декларативный язык записи правил Pulse. Концептуальный наследник идеи «приёмов» Подколзина: каждое правило — узкий приём с явным условием применимости, целью и приоритетом. База правил — это «логическое векторное поле» growth-логики, в котором «движется» текущая ситуация (subject в данный момент).

8.3.1.1 Namespaces в DSL

Выражения в applies_when, forbidden_when и computed-полях правила обращаются к данным через четыре строго разделённых namespace'а:

NamespaceЧто этоВерсионированиеResolve at
subject.tier, subject.country, subject.archetype, subject.opted_out(...)Базовые атрибуты subject'а из identity-mapping + consent store.Не версионируется, snapshot.Decision time, ~ms latency.
subject.features.<metric>@v<n>Lookup в Feature Store. Имя метрики и version — из Semantic Layer (§9.2). Обязательный @v при breaking-изменении определения; без @v берётся latest (DSL-warning).Per-metric semver.Decision time, online store lookup.
ml.<model>@v<n>Output ML-модели: scalar или structured (с confidence band). Имя модели и version — из ML registry (§8.2).Per-model semver.Decision time, model inference.
consent.<purpose>Булева проверка действующего consent на purpose из реестра (§9.1.2).Не версионируется, snapshot.Decision time, consent-store cache.
rate_limit_hit(rule_id, subject, window) и forbidden_when helpersУтилитарные функции, реализованные runtime'ом DSL.Часть DSL версии.Decision time.

Архитектурное правило для namespace'ов:

  • subject.archetype (§9.2.6) — единственный subject-attribute, классифицированный как soft_preference (см. §9.2.6.1 marker). Compile-time запрещён в applies_when и forbidden_when. Используется только в template_variants[*].archetype_affinity и в rule.patterns[*].archetype_affinity для ranking.
  • subject.features.* vs ml.*: feature — это результат вычисления над сырыми событиями по определению из Semantic Layer (детерминирован при том же входе); ml — это output статистической модели (вероятностный, с calibration). Это разные namespace'ы намеренно, чтобы audit log явно различал «factual feature» и «model prediction».
  • subject.features.<metric> требует metric_version из Semantic Layer; resolve через online Feature Store. ml.<model> требует model_version из ML registry; resolve через ML inference cache.
  • При недоступности любого namespace'а в decision time — поведение управляется null_handling правила (default: pass_through_to_forbidden_when если есть match, иначе rule skipped с warning).

Compile-time check: rule, ссылающийся на несуществующее имя в любом namespace, отклоняется при validate. SMT-валидатор (§8.3.5) проверяет, что namespace'ы используются согласно своим разрешениям.

8.3.2 Структура правила

yaml
rule:
  id: R0042
  name: "Pro user with low Veil traffic → relay upsell"
  description: "..."
  priority: 7

  on_event:                  # триггер: какое событие активирует
    type: veil.session_ended

  applies_when:              # condition (boolean expression)
    all:
      - subject.tier == "pro"
      - subject.features.veil.session_count_7d < 3
      - subject.features.veil.bytes_in_30d_avg < 100_000_000
      - ml.churn_prob_30d > 0.5

  then:                      # действие
    propose_action: show_ui_banner
    params:
      banner_id: "veil_relay_upsell_v3"

  forbidden_when:            # hard constraints (всегда уважаются)
    any:
      - consent.personalization == false
      - subject.opted_out("marketing")
      - rate_limit_hit("R0042", subject, "7d")

  experiment:                # опциональная привязка к эксперименту
    id: exp_relay_upsell_v3
    variant_required: treatment

  patterns:                  # опциональная привязка к marketing patterns
    - PV0001                 # Эффект края — verifier применит constraints
    - PV0002                 # Эффект Миллера
    - PV0011                 # Управляемый эталон

  template_variants:         # опциональные варианты шаблона с soft archetype ranking
    - id: t_fighter
      archetype_affinity: fighter
    - id: t_pragmatist
      archetype_affinity: pragmatist
    - id: t_generic           # обязательный fallback для null archetype / mismatch
      archetype_affinity: any

Расширенная семантика полей:

  • patterns — список ID из marketing-patterns.md. При наличии:

    • Brand-guardrails verifier (§8.5.4) применяет соответствующие composition constraints к template шаблона, на который ссылается action.
    • Audit log записывает активированные patterns как часть decision_context.
    • LLM при объяснении решения может сослаться на pattern для прозрачности маркетолога / пользователя.
    • Pattern affinity ranking: если у нескольких применимых rule различные patterns с разной archetype_affinity к текущему subject'у — Pulse предпочитает rule с лучшим affinity (см. §9.2.6 как soft preference). Это ranking signal, не gating.
  • template_variants — опциональные альтернативные шаблоны одного и того же action. Выбор variant'а — soft ranking по archetype_affinity:

    • При наличии subject.archetype ≠ null — выбирается variant с лучшим affinity match. При равенстве — детерминированно по id.
    • При subject.archetype == null (нет consent или archetype не выведен) — выбирается variant с archetype_affinity: any (fallback).
    • Обязательно наличие хотя бы одного variant с archetype_affinity: any (compile-time check).

Важно: archetype никогда не используется как hard condition в applies_when. Subject c определённым archetype не исключается из применимости правила — только меняется, какой именно variant получит и как ранжируются альтернативы. Это сознательное архитектурное решение: см. §9.2.6 «archetype как soft preference signal».

Privacy compile-time check: rule с template_variants дискриминирующего характера (т.е. archetype_affinity влияет на side_effect класс billing_state_change / tier_change) — отклоняется при validate. Variant может менять только tone/copy/visual, не размер кредита, не цену.

8.3.3 Семантика выполнения

Поток выполнения для конкретного субъекта в конкретный момент:

   event arrives → activate rules where rule.on_event matches
                 → group by priority, descending
                 → for each priority group, eval applies_when
                 → drop rules where forbidden_when triggers
                 → among remaining, optional ML scoring for ranking
                 → top rule (or top variant per experiment) → propose_action
                 → propose_action → Action Contract verifier
                 → if accepted → audit log → action dispatched

Если несколько правил одинакового приоритета проходят все проверки — применяется первое по id (детерминированно), остальные логируются как also_matched.

8.3.4 Места выполнения DSL

DSL компилируется в общий байткод, но исполняется на трёх runtime'ах с разными правами:

RuntimeДоступные actionsДоступные features
Client (App)UI-уровневые: show_ui_banner, local_notif, ...Локальные счётчики + последний central-snapshot
Edge NodeFederation: cap_traffic, priority_route, ...Federation-метрики, peer-stats
Central (billing)Все biling actions, outbound communicationsПолный Feature Store

Контракт-компилятор проверяет, что каждое правило ссылается только на actions/features, доступные на его target runtime, и помечает правило соответствующим тегом. Compile-time гарантия.

8.3.5 SMT-проверка консистентности

При publishing новой версии Rules Registry CI запускает SMT-валидатор:

  • Поиск противоречий (правила, которые при некоторых subject-stat'ах одновременно требуют и forbid одно и то же).
  • Поиск dead rules (правил, чьё applies_when unsat при любых subject-stat'ах).
  • Поиск rate-limit overflows (комбинаций правил, дающих суммарную частоту выше fairness-cap).
  • Поиск budget overflows (комбинаций promo-правил, превышающих заложенный бюджет при ожидаемом распределении subject'ов).

Не прошедшие проверку правила блокируются от выкатки в pulse/main.

8.3.6 Эволюция правил

Каждое правило версионируется отдельно (R0042.v3). Audit log хранит ссылку на конкретную версию правила, применённую к каждому решению. Откат правила — это publish новой версии Rules Registry с тем же id, но applies_when: { never: true } или удаление.

8.4 Experiment Layer

8.4.1 Назначение

Безопасное исследование пространства действий: A/B-тесты для статической рандомизации, bandit-алгоритмы для адаптивного выбора, holdout для каузальной оценки.

8.4.2 Структура эксперимента

yaml
experiment:
  id: exp_relay_upsell_v3
  hypothesis: "Banner v3 повышает conversion в Pro+relay на 10%"
  status: running
  starts_at: 2026-05-01
  ends_at: 2026-06-01

  population:
    include:
      - subject.tier == "pro"
      - subject.country in ["DE", "FR", "NL"]
    exclude:
      - subject.opted_out("experiments")
    size_target: 5000

  variants:
    - id: control
      weight: 0.5
      bound_rule: null
    - id: treatment_v3
      weight: 0.5
      bound_rule: R0042

  metrics:
    primary: relay_purchase_within_7d
    guardrails:
      - subject.churn_30d  # не должен расти
      - subject.complaint_rate

  assignment:
    method: hash_split        # static A/B, не bandit
    salt: "exp_relay_upsell_v3"

8.4.3 Assignment

  • Static (hash_split): bucket = hash(salt || subject_id) mod buckets. Стабильно по времени, выполняется на клиенте без обращения к серверу.
  • Bandit (Thompson sampling, UCB): распределение весов variants обновляется в central; клиент получает текущие веса через signed-channel (как контракт).
  • Holdout: глобальный (~5%) опт-аут, никаким экспериментам не подвергаются — для каузальной baseline-оценки.

8.4.4 Statistical engine

Подсчёт p-values, confidence intervals, sequential testing (mSPRT) — в central. Поверх Bayesian (PyMC/NumPyro) для метрик с приорами; classical для остальных. Результаты публикуются в Pulse internal dashboard + Audit Log.

8.4.5 Guardrails

Каждый эксперимент обязан декларировать guardrail metrics — те, рост которых считается провалом эксперимента независимо от primary. Автоматический stop при нарушении guardrail с заранее настроенным порогом.

8.4.6 Bandit и rules

Bandit не заменяет rule engine. Bandit выбирает внутри допустимого пространства, заданного правилами. Если правило R0042 разрешено для subject и привязано к variant treatment, bandit может в этот момент назначить subject в control — тогда R0042 не применяется, аудит фиксирует «held out by experiment».

8.5 LLM Interface

8.5.1 Назначение

Языковой слой над декларативными артефактами Pulse:

  • NL queries маркетолога над Semantic Layer и Audit Log.
  • Copy generation (тексты писем, баннеров) с brand-guardrails.
  • Explanations для маркетолога / пользователя / аудитора («почему этому subject'у показали X»).
  • Rule drafting: LLM предлагает draft правила по NL-описанию маркетолога; human approves before publishing.

8.5.2 LLM Gateway

Единственная точка обращения к внешним LLM-провайдерам. Функции:

  • PII stripping: текст и параметры запроса проходят через классификатор + regex-фильтры. Любое поле, помеченное pii: true в активном Telemetry contract, не покидает gateway.
  • Tool catalog injection: gateway инжектирует в системный prompt только те tools, на которые роль текущей сессии имеет permission (subset Action contract).
  • Audit: каждый запрос (prompt hash + response hash + tools-called) логируется в Audit Log.
  • Caching: повторяющиеся запросы (например, объяснение того же decision_id) кэшируются по prompt-hash для cost-control.
  • Rate limiting: per-user, per-cost-unit.

8.5.3 Доступные tools для LLM

Через tool calling LLM имеет доступ к:

  • read_metric(name, subject_id_or_cohort, window) — read-only по Semantic Layer.
  • read_audit(filters) — read-only по Audit Log (с учётом ACL роли).
  • simulate_rule(rule_yaml, cohort) — dry-run правила без применения.
  • draft_email(template_kind, audience, brand_voice) — генерация текста с guardrail-проверкой.
  • propose_rule(nl_description) → возвращает draft yaml + summary; не публикует, человек должен apply.
  • explain_decision(decision_id) → детерминированная сборка контекста + LLM-перевод на NL.

Никакие write-actions из Action Contract напрямую LLM не доступны. Все write-операции проходят через явный human approval.

8.5.4 Brand guardrails

Decoding-time constraints для copy generation + post-generation verifier для любых outbound templates (не только LLM-сгенерированных). Проверки делятся на два класса.

Класс A — Brand/legal guardrails:

  • Запрещённые claims (например, «100% безопасно», «никто никогда не узнает» — юридические риски).
  • Запрещённые tone elements (агрессивная urgency, fearmongering, ложная срочность).
  • Обязательные элементы (unsubscribe link для marketing-email; sender address для EU; и т.п.).
  • Запрещённое прямое отрицание стереотипов категории (см. PV0051 в marketing-patterns.md).

Класс B — Composition constraints (из marketing pattern library):

Машинно-проверяемые ограничения на структуру шаблона, привязанные к marketing patterns (см. §17.15). При создании template указывается список применяемых patterns; verifier применяет соответствующие constraints автоматически:

ConstraintПроисхождение patternЧто проверяет
cognitive_load_max: NPV0002 Эффект МиллераКоличество отличных «идей-ударений» ≤ N (default 5)
first_block_carries_key_ideaPV0001 Эффект краяПервый блок шаблона несёт ключевую идею
last_block_carries_key_ideaPV0001 Эффект краяПоследний блок усиливает или резюмирует ключевую идею
expectation_payoffPV0020 ОттяжкаЕсли headline ставит вопрос — payoff присутствует в последнем блоке
series_credibilityPV0010 Кредит доверияВ серии ≥4 marketing-сообщений ≥1 содержит acknowledged limitation
direct_denial_forbiddenPV0051 Коррекция Ст−Шаблон не содержит прямого отрицания негативного стереотипа категории
abstract_groundedPV0052 Ярлык-образАбстрактные термины (privacy/security/encryption) при первом упоминании сопровождаются physical analogy
social_proof_anonymizedPV0040 Чужая победаНикаких конкретных идентифицируемых subject'ов без opt-in
threat_factualityPV0041 Общий врагУтверждения об угрозах документированы со ссылкой
fearmongering_scorePV0041 Общий врагМера эмоционального давления ниже порога
paranoia_languagePV0071 Privacy-hygieneЗапрет паранойного языка («все следят»)

Implementation: post-generation verifier (regex + classifier + LLM-as-judge) + re-generation loop с budget на повторные попытки. Verifier работает как часть pulse-core; вызывается:

  • В POST /api/templates/validate при сохранении шаблона маркетологом в Directus.
  • Перед dispatch action'а — финальная проверка с подстановленными params.
  • В meta-automation pipeline (champion-challenger) для новых template-версий.

Audit: каждое срабатывание guardrail регистрируется как audit_class: brand_guardrail_violation с reference на template_id, pattern_id, constraint_kind. Это даёт лог «почему промо v3 не пошёл — нарушил cognitive_load».

Полный каталог composition constraints с источниками — в marketing-patterns.md §3.

8.5.5 Локальный LLM (future)

В milestone M4 предусматривается опциональный локальный LLM (на стороне клиента или owner-Node) для пользовательских объяснений, где обращение к внешнему провайдеру нежелательно. Архитектурно LLM Gateway уже спроектирован как pluggable backend.


9. Cross-cutting горизонтали

Три слоя, пронизывающих все основные. Без них основной pipeline технически работает, но юридически, операционно и аналитически — нет.

9.1.1 Identity-mapping service

Отдельный микросервис в central, единственный, кому известно соответствие (identity_id, device_id, pairing_id) ↔ subject_id. Изолирован от остальных компонентов Pulse:

  • Отдельная БД с отдельным шифрованием at rest.
  • Доступ только через узкий API (resolve_subject(identity_id) → subject_id, pseudonymize(event) → event_with_subject).
  • Логирование каждого обращения с reason.
  • Никаких dump-эндпоинтов.

Subject_id генерируется как blake3(identity_id || global_salt_v{N}) где global_salt_v{N} периодически ротируется. При ротации создаётся новая generation; старые subject_id остаются read-only для исторических данных. Это даёт возможность «забыть» субъекта целиком, не теряя референсной целостности audit-log (вместо subject_id остаётся хеш-метаданные).

Декларативный список целей сбора и обработки данных. Часть контракт-канала (отдельный YAML-документ, ссылается из Telemetry contract).

yaml
purposes:
  analytics.usage:
    description: "Аналитика использования продукта для улучшения функциональности"
    legal_basis: legitimate_interest
    default: opt_in
    required_for_product: false
    data_categories: [usage_metrics, session_aggregates]

  personalization:
    description: "Адаптация интерфейса и предложений под пользователя"
    legal_basis: consent
    default: opt_in
    required_for_product: false
    data_categories: [usage_metrics, preferences]

  experiments:
    description: "Участие в A/B-тестах и продуктовых исследованиях"
    legal_basis: consent
    default: opt_in
    required_for_product: false
    data_categories: [usage_metrics, assignment_logs]

  marketing.email:
    description: "Получение marketing-сообщений по email"
    legal_basis: consent
    default: opt_in
    required_for_product: false
    data_categories: [email_address, send_history]

  ops:
    description: "Минимальная техническая телеметрия (ошибки, crashes)"
    legal_basis: legitimate_interest
    default: opt_in
    required_for_product: true
    data_categories: [error_traces, crash_reports]
    notes: "Опт-аут возможен, но может ухудшить качество саппорта."

Важно: этот блок — базовый subset purposes, описанный для иллюстрации структуры registry. Полный реестр живёт в коллекции pulse_curated.consent_purposes (Directus-managed) и расширяется по мере подключения новых каналов. По состоянию на v0.9 предусмотрены также следующие purposes (полная спецификация — в pulse_curated.consent_purposes schema, не дублируется здесь):

PurposeТриггер появления (раздел спеки)Канал / use case
marketing.push§17.4.3 (visible push notifications)OS-level push, marketing-уровня
marketing.email_engagement_tracking§17.4.2 (email open / click tracking)Pixel tracking, отдельный opt-in поверх marketing.email
marketing.attribution§17.8 (paid ads, server-side conversion)Privacy-preserving conversion reporting в ad-networks
local_notifications§17.3.5 (locally-scheduled notifications)OS notifications, генерируемые app локально
external_identity_linking§17.6.1 (Discord/Matrix opt-in linking)OAuth-pairing внешних identity для community-analytics

Каждый purpose заводится в коллекции с полным набором полей (description, legal_basis, default, data_categories, etc.) — см. §13.4 «Consent storage». Consent matrix per channel — в §17.13.

Хранит для каждого subject'а:

  • Список granted purposes с timestamp grant + источник (UI / Pairing / Admin).
  • Expiration (если purpose имеет ограничения по сроку).
  • История изменений (append-only).

Реализация: таблица в существующем billing/ Postgres. Pulse-core читает через узкий API, который при возможности кэширует на короткое окно (минуты).

Каждое событие, эмитированное клиентом, сопровождается consent_token:

consent_token := sign(client_key, {
  subject_id,
  purpose,
  granted_at,
  token_issued_at,
  expires_at  # короткий, минуты-часы
})

Ingest при приёме события:

  1. Проверяет подпись токена клиентским ключом.
  2. Сверяет purpose с purpose'ом event-типа из Telemetry contract.
  3. Сверяет granted_at с Consent Store (защита от replay устаревшим токеном).

Это даёт non-repudiable доказательство наличия consent в момент эмита, независимо от того, что произошло потом.

Точная спецификация формата токена. Это нормативная часть; codegen и валидаторы пишутся отсюда.

Encoding. CBOR (RFC 8949), deterministic profile (RFC 8949 §4.2.2). Не JSON — компактнее, быстрее, без неоднозначностей кодирования чисел. Транспортируется в HTTP-заголовке X-Consent-Token: base64url(cbor_envelope).

Envelope. COSE_Sign1 (RFC 9052), alg = EdDSA с Ed25519. Структура:

COSE_Sign1 = [
  protected_headers:   bstr (CBOR map),
  unprotected_headers: {},
  payload:             bstr (CBOR map — claims),
  signature:           bstr (64 bytes Ed25519)
]

protected_headers (CBOR map с integer keys, RFC 9052):

KeyNameValue
1alg-8 (EdDSA)
4kidbstr — client_key_id; 32 bytes BLAKE3 публичного ключа клиента
33purposetstr — purpose ID из consent-purposes.yaml

Claims (CBOR map с string keys в payload):

ClaimTypeRequiredОписание
subbstr (32)yessubject_id — то же значение, которое identity-mapping вернёт ingest'у
genu16yesgeneration salt'а identity-mapping
purtstryespurpose ID (дубль из protected headers для удобства парсинга)
gatu64yesgranted_at — unix epoch seconds, момент grant'а в Consent Store
iatu64yestoken_issued_at — момент создания токена
expu64yesexpires_at — unix epoch seconds
jtibstr (16)yesunique token id (BLAKE3-derived from `iat
nbfu64nonot_before (RFC 7519); если present, токен невалиден до nbf

TTL и refresh. exp - iat ≤ 3600 (1 час). App обновляет токен лениво — при первом эмите события через 50 минут после iat запрашивается новый токен у App-side Consent client (см. §13.5). Клиентское время синхронизировано через NTP; tolerance ±60 s.

Replay protection.

  • Ingest держит in-memory LRU cache jti → seen_at размером 1 M записей с TTL = max_token_lifetime (1 ч). При collision — 400 + events_rejected.reason = 'malformed_token'.
  • На уровне ingest-кластера cache per-instance (не distributed) — обусловлено load balancer affinity по kid.

Clock skew. Ingest принимает токен если:

now() - 60s ≤ iat ≤ now() + 60s
now() - 60s ≤ nbf  (если present)
exp > now() - 60s

При нарушении — events_rejected.reason = 'malformed_token' (НЕ consent_expired — отличаем формальную проблему токена от истечения consent'а как такового).

Client key location.

ПлатформаХранилищеЖизненный цикл
iOS / macOSKeychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)Generated при первом запуске; ротация при app.tier_changed или раз в 90 дней
AndroidAndroid Keystore (KeyProperties.PURPOSE_SIGN, StrongBox если есть)То же
WindowsDPAPI / CredentialManager (per-user)То же
LinuxStronghold (см. docs/app/services/stronghold.md)То же
Headless / CLIStronghold с user-supplied passwordmanual rotation

client_key_id = BLAKE3(public_key) регистрируется в Consent Store при первом grant'е и связывается с (subject_id, device_id, generation). При обнаружении неизвестного kid ingest пытается зарегистрировать его лениво через identity-mapping (требует подписи pairing-токеном от App-side Identity manager); при невозможности — events_rejected.reason = 'malformed_token'.

Реализация. Используем существующий crate coset для COSE и ed25519-dalek для подписи. Канонизация CBOR через профиль deterministic encoding (RFC 8949 §4.2.2) — обязательна, иначе подпись невоспроизводима.

Test vectors. Корпус тестовых токенов с known keys и expected results лежит в test-corpus/consent-tokens/ (см. testing.md §12.1). Содержит: golden valid token, expired token, future-dated token, wrong-purpose token, mismatched-subject token, replay-attempt pair.

9.1.5 Right to erasure

При получении erasure-request:

  1. Identity-mapping service помечает (identity, subject_id) mapping как удалённый.
  2. Pulse-core запускает batch удаления всех записей с этим subject_id из Feature Store и Online Store.
  3. Audit Log не модифицируется (append-only invariant); записи с этим subject_id остаются, но subject_id уже не resolvable — становится «висящим псевдонимом».
  4. Decision'ы и derived модели, использовавшие удалённого subject'а в training set'е, помечаются для re-train при следующем cycle'е.

9.1.6 Identity-mapping service API

Узкий gRPC + HTTP API. Контракт фиксирован в contracts/openapi.yaml (HTTP) и contracts/identity-mapping.proto (gRPC, M1+); ниже — операционная семантика.

Операции:

OperationCaller (allowed)ArgsReturnsSide effects
pseudonymizepulse-core-ingestidentity_id, event_tssubject_id, marketing_subject_id?, generationСоздаёт mapping, если не существует; пишет в access_audit
resolve_subjectpulse-core-decisioning, pulse-core-gdprsubject_id, generation, reasonidentity_id (только для GDPR) или OK-маркерaccess_audit row
resolve_marketingpulse-core-decisioningsubject_id или marketing_subject_idпарный псевдонимaccess_audit row
erase_subjectpulse-core-gdprsubject_id, reasonOKУстанавливает erased_at; mapping становится нерезолвимым; access_audit
rotate_saltpulse-admin-cli (root)новый generationСоздаёт новый salt_generations row; обновляет active_generation
list_generationsлюбой allowed callerсписок generationsread-only

Аутентификация и авторизация:

  • Каждый caller имеет Ed25519 keypair, зарегистрированный в identity_mapping.allowed_callers.
  • Каждый запрос подписан Ed25519 (Authorization: Bearer <jwt-like-token> с подписью header+payload).
  • Сервис проверяет: (а) подпись валидна, (б) caller_service имеет операцию в allowed_ops, (в) запрос не replay (jti в коротком окне).
  • На каждый запрос — синхронная запись в access_audit (даже на deny — особенно на deny).

Изоляция БД:

  • Отдельный Postgres instance (не shared с billing.* / pulse_runtime.* / pulse_curated.*).
  • Учётные данные — только у identity-mapping service.
  • Сетевая изоляция: trusted internal network; нет публичных endpoints.
  • Backup: encrypted-at-rest, отдельный ключ от Pulse main DB; restore требует двух операторов.

Производственные инварианты:

  • subject_id НИКОГДА не покидает identity-mapping в обратной операции для не-GDPR callers. resolve_subject для pulse-core-decisioning возвращает только OK-маркер (доказательство существования), не identity.
  • marketing_subject_id — отдельный псевдоним, видимый growth pipeline; mapping subject_id ↔ marketing_subject_id известен только этому сервису.
  • Salt-ротация: старые generations резолвятся read-only до момента полного пересчёта feature store на новый generation (см. §10.6 bootstrap procedures).

Rate-limit и DoS-защита:

  • Per-caller token-bucket (count + window из конфигурации); превышение — 429 + access_audit(denied).
  • На уровне сервиса — глобальный circuit-breaker (при p99 latency > budget из §10.7).

9.2 Semantic Layer

9.2.1 Назначение

Single source of truth для определений метрик и бизнес-терминов. Все runtime'ы (ingest, ML, DSL, LLM, dashboards) ссылаются на одинаковые определения, что устраняет дрейф семантики между подсистемами.

9.2.2 Структура реестра

Отдельный YAML-документ, привязанный к контракт-каналу.

yaml
contract: semantic
version: 0.4.0

dimensions:
  subject_id:    { type: subject }
  tier:          { type: enum[free, standard, pro, pro_plus] }
  country:       { type: country_code }
  pairing_age:   { type: duration_days }

metrics:
  active_user:
    description: "Subject с ≥1 успешной сессией Veil или Tether за окно"
    type: boolean_per_subject
    window: rolling_7d
    sources:
      - event: veil.session_ended
        filter: status == "success"
      - event: tether.session_ended
        filter: status == "success"
    consent_required: analytics.usage
    version: 3
    available_from: 2026-01-01

  churn_30d:
    description: "Subject не был active_user в течение 30 дней"
    type: boolean_per_subject
    depends_on: [active_user]
    formula: "not active_user.over(30d)"
    consent_required: analytics.usage
    version: 2

  veil_intensity:
    description: "Среднее количество сессий Veil в день за 7 дней"
    type: float_per_subject
    window: rolling_7d
    sources:
      - event: veil.session_started
    aggregation: count() / 7
    consent_required: analytics.usage

  revenue_per_subject:
    description: "Подтверждённый MRR за subject"
    type: money_per_subject
    sources:
      - event: billing.payment_succeeded
        field: amount
    aggregation: sum.over(rolling_30d) / 1
    consent_required: ops  # биллинг — operational, не маркетинговый

9.2.3 Версионирование метрик

Изменение определения метрики = новая версия (version: 4). Старые версии остаются доступны для исторических вычислений и replay. Audit Log хранит metric_version, использованную при принятии решения.

DSL-правила и ML-модели объявляют, на какую версию метрики они опираются: subject.features.active_user@v3. Это компилируется в строгий type-check; правило, использующее устаревшую версию, помечается deprecated.

9.2.4 Lineage

Из определений генерируется граф lineage:

veil.session_ended ─┐
                    ├─► active_user ─┬─► churn_30d ─┬─► R0042
tether.session_ended┘                │              │
                                     └────────► ML.churn_prob_30d

Граф используется:

  • При invalidation: изменение event-типа → пересчёт всех derived metrics → re-train зависимых моделей → re-validation правил.
  • В audit log для traceability «откуда взялся этот score».
  • В dashboards для отображения health of metrics pipeline.

9.2.5 Codegen

Из Semantic Layer YAML генерируются:

  • Rust типы Metric* для DSL-runtime.
  • TS типы для маркетингового UI.
  • SQL views в Feature Store.
  • Cube.dev / dbt model (если используется внешний BI-инструмент).

9.2.6 Archetype как soft preference signal

В рамках интеграции маркетинговых паттернов (§17.15 и marketing-patterns.md) Semantic Layer содержит особое поле subject.archetypesoft preference signal для ranking patterns, template variants и onboarding flows. Это не condition-dimension: ни одно правило не использует archetype в applies_when.

9.2.6.1 Определение
yaml
dimension: archetype
  type: enum[fighter, pragmatist, observer, evangelist, refugee, null]
  category: soft_preference          # явный маркер, не condition-dimension
  consent_required: personalization  # без consent — всегда null
  derivation:
    kind: self_reported_only
    from_event: app.onboarding.archetype_survey_responded
    refresh_via: app.settings.archetype_updated
  null_handling: fallback_to_generic
  version: 1

Только self-reported source. В v0.7 derivation сознательно сведена к опциональному survey-вопросу в onboarding и редактируемому полю в Settings → Privacy. Никаких behavioural-сигналов, attribution-bias, ML-кластеризации — это было overengineering (archetype settled by design, v0.8).

9.2.6.2 Где archetype используется
  1. Pattern affinity ranking (§17.15). При выборе между несколькими применимыми правилами Pulse предпочитает rule, привязанные patterns которого имеют лучший archetype_affinity к subject'у.
  2. Template variant selection (§8.3.2). Внутри одного rule с template_variants выбирается вариант с подходящим archetype_affinity. Обязательный fallback archetype_affinity: any.
  3. Onboarding flow variant. Onboarding-action start_onboarding_flow (§17.3.2) поддерживает personalization через стандартный механизм template_variants с archetype_affinity (§8.3.2). Никакого специального параметра в action contract не требуется — onboarding обрабатывается как любой другой action с template-вариантами.
  4. Cohort analytics. Descriptive: retention/conversion by archetype в Insights. Только агрегаты, никакого per-subject экспорта.
9.2.6.3 Где archetype НЕ используется (compile-time forbidden)
  • applies_when правил — archetype не condition, а preference.
  • Действия класса billing_state_change, tier_change, billing_credit — variant нельзя различать по archetype в денежных величинах.
  • Excluding subjects из применимости — нет «gating by archetype». Subject любого archetype получает функцию, отличаются только варианты сообщения / порядок шагов.
9.2.6.4 Privacy-инвариант (расширение §3.1)
  1. Archetype требует consent.personalization. При отсутствии — archetype: null. В этом случае все rules работают в generic-режиме (выбирается variant archetype_affinity: any).
  2. Subject видит и редактирует свой archetype через App → Settings → Privacy. Это часть GDPR right-to-access + right-to-rectification.
  3. Archetype не используется в pricing/billing. Compile-time check блокирует попытки.

Что archetype НЕ:

  • Не permanent label.
  • Не входит в decision как hard condition.
  • Не выводится автоматически из поведения.
  • Не используется без явного consent.
9.2.6.5 Почему survey-only

Альтернатива (ML/behavioural derivation) даёт малый прирост качества при существенных издержках:

  • Сложная инфраструктура (cluster training, drift detection, model versioning).
  • Регуляторный риск (psychographic profiling без consent на конкретные signals).
  • Goodhart-эффект (subject может «оптимизировать» поведение под желаемый archetype, если автоматическое derivation видно).
  • Низкая explainability.

Self-reported survey даёт:

  • Прозрачность для subject'а (он сам выбрал).
  • Простую legal позицию (явный consent на конкретное использование).
  • Дешёвую реализацию (один survey в onboarding + setting в UI).
  • Лёгкую коррекцию (subject в любой момент меняет).

Это базовый принцип: archetype полезен не точностью классификации, а наличием явно выраженного предпочтения subject'а.

9.2.7 Timestamps вместо derived-from-now counters

В Semantic Layer запрещены materialized метрики вида «days/weeks/months since event X», рассчитываемые от now(). Это design-инвариант с тремя причинами:

  1. Stale data. Любой derived-from-now() аггрегат имеет refresh-задержку (batch / continuous_aggregate). Decision engine, читающий «days_since_install = 5», может работать с реальным значением 6 — rule, ожидающий «≥ 7», не сработает до следующего batch run'а. На границах суток это лотерея.

  2. Не replay-able. Decisions воспроизводятся через pulse_runtime.decisions row + восстановление input feature snapshot. Если фича derive'ится от now(), при replay через неделю значение другое — то же решение даст другой output. Это ломает audit reproducibility (см. §9.3) и regression-тесты (см. testing.md §5.5).

  3. Bucket-freeze. Сегодня бизнесу нужно «по дням», завтра — «по часам» (для in-day follow-up), послезавтра — «по месяцам». Каждая новая гранулярность требует нового metric + версии. Если хранить момент, любой bucket вычисляется в DSL: subject.installed_at < now() - 4h, subject.installed_at < now() - 30d, и т.д.

Правило. Каждое событие, обозначающее «момент» (install, first payment, onboarding completion, last seen, archetype self-report), populating-ует immutable timestamp dimension в feature_store: subject.installed_at, subject.first_paid_at, subject.onboarding_completed_at, subject.last_seen_at, и т.д. DSL и dashboard'ы derive'ят «days since» через time arithmetic at query time.

Исключения.

  • Counts с sliding window (app_sessions_last_30d, email_open_rate@30d) — допустимы как аппроксимация для analytics/cohort dashboards, но запрещены в applies_when rules с tight time deadline. Validator (validate.py, M1+) предупреждает.
  • Counts без now()-зависимости (app_sessions_total, paired_devices_count) — допустимы; они монотонно растут и не подвержены batch lag.
  • Global metrics (dau, mau) — допустимы для system dashboards, запрещены в subject.features.* namespace (см. §8.3.1.1).

В v0.1.0 semantic layer. Все «moment»-данные — timestamps (subject.installed_at и пр.). Бывший metric days_since_install удалён; cohort definitions переписаны через time-arithmetic syntax subject.installed_at < now() - Xd.

9.3 Audit Log

9.3.1 Структура записи

yaml
audit_record:
  decision_id: 01HXYZ...                    # ULID
  recorded_at: 2026-05-18T14:32:01Z
  subject_id: subj_b3a7...                  # pseudonymous
  contract_id: b3:7a2f...e91d               # активная версия контракта на момент решения

  action:
    type: send_promo_email
    params_hash: blake3(canonical(params))
    target: app
    audit_class: outbound_communication

  decision_context:
    triggered_by:
      event_id: evt_01HXY...
      event_type: veil.session_ended

    layers_consulted:
      - layer: ml
        model: churn_prob_30d
        model_version: v7.2
        inputs_hash: blake3(...)
        outputs: { churn_prob: 0.62, confidence: 0.81 }

      - layer: rules
        rules_evaluated: [R0042.v3, R0089.v1, R0102.v2]
        rules_matched: [R0042.v3]
        rules_rejected:
          - { id: R0089.v1, reason: forbidden_when(rate_limit) }
          - { id: R0102.v2, reason: priority_lower_than_R0042 }

      - layer: experiment
        experiment_id: exp_relay_upsell_v3
        assignment: treatment_v3
        assignment_method: hash_split

      - layer: llm
        used: false   # для этого решения LLM не вызывалась

  consent_basis:
    purpose: marketing.email
    granted_at: 2026-04-01T08:00:00Z
    token_hash: blake3(...)

  metrics_versions:
    active_user: 3
    churn_30d: 2
    veil_intensity: 1

  outcome:
    dispatched_to: app
    target_ack: { status: success, at: 2026-05-18T14:32:02Z }

  prev_hash: blake3(previous audit record)  # Merkle chain link
  record_hash: blake3(this canonical record)

9.3.2 Append-only Merkle chain

Audit Log — append-only лог в central Postgres (или dedicated ClickHouse при росте). Каждая запись хэш-цепляется с предыдущей: prev_hash поле.

Каждые N записей (или каждые M минут) формируется Merkle root партии и публикуется в Kontinuum DHT по ключу pulse-audit-root:{epoch} с подписью Tier 0 (см. §11.4). Это даёт внешне-проверяемую неизменность: любой третий участник может скачать root'ы и убедиться, что central не подделал историю.

9.3.3 Уровни доступа

Audit classКто читает (по ролям)
ui_personalizationPulse devs, marketing, sec-audit
outbound_communicationPulse devs, marketing, compliance, sec-audit
billing_creditPulse devs, finance, compliance, sec-audit
tier_changePulse devs, finance, compliance, sec-audit, human-approval queue
external_provider_callPulse devs, sec-audit
metaPulse devs, sec-audit

actor_kind resolution rule. Когда audit row создаётся:

  • triggered subject action (rule с requires_consent, реактивный response на event) → actor_kind = subject, actor_id = hex(subject_id).
  • triggered admin in Directus UI (rule deploy, template approval, manual erasure) → actor_kind = admin, actor_id = directus_user_id.
  • triggered self (meta-actions, log_only, gdpr-worker, retention-worker, pulse-core internal) → actor_kind = system, actor_id = <service_name> (e.g. pulse-core-decisioning, pulse-retention-worker).
  • triggered federation peer (DHT pointer ingestion) → actor_kind = federation_peer, actor_id = hex(peer_id).

log_only actions, dispatched from any rule, всегда записывают actor_kind = system независимо от того что rule сработал на subject event — потому что log_only не имеет visible side effect на subject.

Subject имеет право запросить свою часть audit log через GDPR right-to-access (через App UI), получая выгрузку записей с его subject_id.

9.3.4 Replay и reproducibility

По decision_id можно:

  1. Поднять contract_id, скачать артефакт из DHT.
  2. Восстановить версии моделей, правил, метрик из лога.
  3. Запустить decision-engine в replay mode с теми же входами.
  4. Сверить, что результат идентичен записанному.

Это используется:

  • Для дебага «почему этому subject'у показали X».
  • Для регуляторного аудита.
  • Для обнаружения дрейфа в LLM-output: если replay даёт другой output на тех же входах, это сигнал к коррекции prompt'ов / модели.

9.3.5 Retention

Audit log хранится:

  • > 7 лет для billing_* и tier_* классов (требование финансового аудита).
  • > 3 года для outbound_* (compliance for marketing communications).
  • > 1 год для остальных.

После истечения retention запись не удаляется физически — обрезаются её детальные поля, остаётся только hash-метаданные. Merkle-цепочка целостна навсегда.


10. Места выполнения (deployment)

Pulse — распределённая система. Каждый слой имеет явное место выполнения, определяемое privacy- и performance-требованиями.

10.1 Уровни деплоя

УровеньЧто этоДоверие к данным
Client deviceApp, Veil, Tether, Send. Источник сырых данных.Полное: все данные доступны в plaintext.
Owner Node (Tier 0/1 self / org)Узлы, принадлежащие subject'у или org'у.Доверенные псевдонимизированные данные.
Community Node (Tier 2)Узлы третьих лиц.Не доверены: только инвариантные операции.
Central (Pulse Core)Кластер сервисов команды платформы.Доверен в плане pseudonymized data.
External LLMAnthropic / OpenAI / другие.Не доверен; PII-strip обязателен.

10.2 Распределение слоёв по уровням

Слой / компонентClientOwner NodeCommunity NodeCentralExternal LLM
Raw events✓ keep
Telemetry emit
Consent token signing
On-device aggregation✓ opt
Telemetry relay─ (skip)
Federation-policy DSL✓ inv-only
Ingest + pseudonymize
Feature Store online
Feature Store offline
ML training
ML inference (heavy)
ML inference (light)✓ opt✓ fallback
Policy DSL (UI rules)
Policy DSL (federation)✓ subset
Policy DSL (billing)
Experiment assignmentconfig
Experiment statistics
Identity-mapping✓ sole
Consent Store source
Consent Store replica
Semantic Layer compute✓ types✓ types✓ types✓ computecatalog
Audit Log local copy
Audit Log master + Merkle
LLM Gateway
LLM inference

«inv-only» = только инвариантные правила, не зависящие от данных tenant'ов (например, общая capacity-fairness).

10.3 Tier-aware behaviour

Поведение Pulse меняется в зависимости от tier'а Node, где находится subject:

  • Tier 0 (Anchor): subject'ы команды/org. Никаких ограничений по приватности; полная growth-петля. Используется для дев/тест-данных.
  • Tier 1 (org Billed): стандартный случай. Полная growth-петля при наличии consent. Default deployment scenario.
  • Tier 2 self-hosted (byo:vps / byo:home): PRO-пользователи на своей инфре. Часть слоёв может выполняться на их Node — например, on-node aggregation и Audit Log local copy. Минимизирует объём данных, попадающих в central. Это будущий milestone (M4); v0.1 трактует Tier 2 как обычный source.
  • Tier 2 community-shared (family-mode tenants): subject'ы, использующие чужой Node как relay. Telemetry от них собирается только с opt-in extra consent, потому что host-владелец не должен видеть статистику gostей.

10.4 Минимальная central-инфраструктура

Для v0.1 central Pulse состоит из следующих компонентов. Отдельного desktop/web-приложения Pulse нет — управление осуществляется только через Directus admin (см. §4.4-bis).

Backend-сервисы (без UI):

  • pulse-core: Rust-сервис, объединяющий ingest, DSL runtime, decision engine, action dispatch. Stateless, горизонтально масштабируется. Tokio + axum. Не имеет собственного UI; обслуживает HTTP API для Directus extensions и принимает события от источников.
  • pulse-trainer: Rust-сервис для batch ML training. Запускается по расписанию (Nx + cron / Kubernetes CronJob).
  • pulse-llm-gateway: Rust-сервис-прокси к внешним LLM API (Axum + async-openai/anthropic-rs). Stateless. Используется Directus-модулем Pulse Chat (M4+). Rust выбран ради консистентности backend stack'а (см. user-memory project_stack: «core-крейты Rust»), shared types с pulse-core через tonic/serde, type-safe PII strip enforcement через PiiTagged<T> newtypes, и единый codegen pipeline для tool catalog (action-contract-schema.yaml → LLM tool definitions через build.rs). Prompt iteration не блокируется: prompts хранятся в pulse_curated.templates (data-driven), не в Rust-коде — маркетинг редактирует через Directus admin UI без рекомпиляции gateway.
  • identity-mapping: Rust-сервис, изолированный от core. Отдельная БД. Недоступен Directus admin'у напрямую — Pulse-core делает резолвинг через узкий API при ingest'е.
  • pulse-admin-cli: operator-side CLI-утилита (Rust) для критических операций, требующих root-привилегий и offline-ceremony подписей: ротация identity-mapping salt (§10.6.6), provisioning team-signing keys (pulse_curated.federation_root_keys), emergency halt в DHT. Не запускается как long-running сервис; private key лежит у operator'а (yubikey + Shamir share), public key зарегистрирован в identity_mapping.allowed_callers.

Хранилища:

  • postgres (shared с Directus / Billing): один кластер, несколько схем:
    • directus_system — управляется Directus.
    • billing — управляется Directus.
    • pulse_curated — управляется Directus (rules, templates, experiments, consent_purposes, approval_queue).
    • pulse_runtime — управляется Pulse-core (events, features, decisions, audit). Directus admin имеет доступ только через read-only views с RLS.
    • Permissions через Postgres roles + Row-Level Security.
  • postgres-identity (отдельный инстанс): только identity_mapping.*. Подключается только сервис identity-mapping. Никаких других connection grant'ов.
  • clickhouse (опционально, milestone M2): Feature Store offline + analytics queries при росте объёма.

UI:

  • Directus admin — единственная панель управления для маркетинга, billing и support. Pulse-функциональность добавляется как:
    • Стандартные коллекции pulse_curated.* (M0).
    • Custom interfaces для специфичных полей (M1+).
    • Custom modules для сложных рабочих сценариев — Rule Studio, Experiments Console, Audit Explorer, Pulse Chat (M2-M4).

Все сервисы в одном Nx-проекте kontinuum-pulse/, деплой через Docker Compose / Kubernetes (унифицированно с node/billing). Directus extensions деплоятся вместе с Directus как часть billing-инфраструктуры.

10.5 Offline behaviour

Клиент должен корректно работать без связи с central:

  • Применять локальные DSL-правила.
  • Выполнять experiment assignment локально (hash_split).
  • Буферизовать telemetry до восстановления связи.
  • Использовать локальную копию Consent (read-only) для UI-уровневых проверок.

При восстановлении связи буфер flush'ится в порядке timestamp'ов; ingest корректно обрабатывает «опаздывающие» события (помечает late=true, не использует для real-time decisioning, но включает в training).

10.6 Bootstrap procedures

Процедуры запуска Pulse «с нуля» (M0). Цель — детерминированно поднять связку identity-mapping + pulse-curated + pulse-runtime + Directus + pulse-core в воспроизводимом порядке. Каждый шаг идемпотентен, каждое сгенерированное значение фиксируется в pulse_runtime.audit_log.

10.6.1 Pre-bootstrap (ops-side, off-box)

Эти артефакты должны существовать ДО первого запуска сервисов:

  1. Root signing key для federation channel pointers — Ed25519, генерируется в offline ceremony (ops-team yubikey + Shamir share); публичный ключ зафиксирован в pulse_curated.federation_root_keys через миграцию.
  2. Per-service Ed25519 keypairs для callers identity-mapping: pulse-core-ingest, pulse-core-decisioning, pulse-core-gdpr. Private key — в Stronghold-like secret manager, public key — в seed-миграции identity_mapping.allowed_callers.
  3. Initial global_salt — 32 байта от CSPRNG, фиксируется в identity_mapping.salt_generations(generation=1, salt=…). Никогда не логируется в plain text.
  4. Directus admin — учётная запись с MFA, отдельная роль pulse-admin создана через миграцию Directus с правами на pulse_curated.*, read-only на pulse_runtime.*.
  5. TLS-сертификаты для внутренних gRPC между сервисами (mTLS, отдельный CA для internal mesh).

10.6.2 Database bootstrap

Порядок применения миграций (через dbmate/sqlx migrate, контролируемо CI):

  1. identity_mapping Postgres (отдельный instance):

    • 01_identity_mapping.sql — схема, таблицы, indices. Также содержит inline INSERT для allowed_callers (placeholder-ключи; production-ключи перезаписываются через seed_allowed_callers_prod.sql, генерируемый CI из vault) и инициализирует salt_generations(generation=1) + active_generation через bootstrap-job pulse-admin-cli (см. §10.6.6).
  2. Shared Postgres (вместе с billing.*):

    • 02_pulse_curated.sql — Directus-managed схема (включая federation_root_keys).
    • 03_pulse_runtime.sql — pulse-core-managed схема.
    • seed_audit_genesis.sql — записывает audit_log_genesis.root_hash = blake3("pulse-audit-v1" || instance_id). Все последующие audit_log rows цепляются к этому корню.
    • seed_consent_purposes.sql — наполняет pulse_curated.consent_purposes из contracts/consent-purposes.yaml (контракт-загрузчик читает YAML и делает upsert).
    • seed_marketing_patterns.sql — PV0001…PV0018 из marketing-patterns.md.
    • seed_federation_root_keys.sql — публичные ключи из §10.6.1 (ceremony output).

    Scope note. Файлы 01-03_*.sql лежат в contracts/ddl/ и являются частью freeze package v0.11. Файлы seed_*.sql генерируются bootstrap-pipeline'ом (CI + offline ceremony output) и не входят в репозиторий — это секреты или derived-from-spec артефакты.

10.6.3 Contracts bootstrap

Перед первым запуском pulse-core должны быть опубликованы:

  1. Telemetry contract v0.1.0 — содержит минимальный набор event-типов M0 (veil.*, app.*, billing.*, pulse_internal.*); content-hash вычисляется при загрузке, публикуется в DHT по contract-artifact:{hash}.
  2. Action contract v0.1.0show_ui_banner, send_email, log_only для M0.
  3. Semantic layer v0.1.0 — определения базовых метрик (dau, mau, tier, veil_bytes_total).
  4. Channel pointer pulse-channel:main подписан root signing key'ом и опубликован в DHT.

Pulse-core при старте читает channel pointer, проверяет подписи, загружает artifact'ы, валидирует их по meta-схемам из contracts/*-schema.yaml. Сбой валидации = отказ старта.

10.6.4 Service bring-up sequence

Порядок старта (Docker Compose depends_on / Kubernetes init-containers):

1. identity-mapping        (БД + ACL + initial salt)
2. directus                (БД + admin user + extensions)
3. pulse-core              (validates contracts; пишет первую audit_log row "service.started")
4. pulse-edge (опц.)       (regional ingest gateways)

Каждый сервис при старте делает healthcheck предыдущего и пишет audit_log("service.started", {version, commit, contract_hashes}).

10.6.5 Smoke verification

После bring-up автоматически прогоняется smoke-suite (часть deploy pipeline):

  1. POST /health/ready каждого сервиса → 200.
  2. Test event через /events/ingest с тестовым consent-token → 207 с accepted=1.
  3. Создание тестового rule в Directus → POST /dsl/validate → 200 valid=true.
  4. Чтение audit_log — проверка непрерывности hash-chain (последний prev_hash совпадает с предыдущим entry_hash или genesis).

Любой провал → rollback развёртывания.

10.6.6 Salt rotation procedure

Salt rotation — отдельный bootstrap-like процесс:

  1. Ops оператор вызывает rotate_salt (требует 2 операторов через approval queue + MFA).
  2. Identity-mapping создаёт новую salt_generations row (generation+1).
  3. Обновляется active_generation.
  4. Backfill job: пересчёт subject_id для активных subjects в новом generation; старые generation остаются read-only для исторических данных.
  5. Feature store backfill: новые subject_id дублируют существующие записи (на время transition).
  6. После полного backfill — старые generation помечаются deprecated_at; удаление по retention.
  7. Каждый шаг — отдельная audit-row.

Side effect: sticky sampling reshuffle. sample_strategy: sticky (см. §6.9.2) использует blake3(subject_id || event_type) как seed. После rotation новый subject_id даёт другой seed → subset событий, ранее попадавших в sample, выпадает; другие — входят. Это не баг, но cohort analytics, пересекающие rotation boundary, должны:

  • Использовать generation-aware aggregation (split по generation в SQL window),
  • ИЛИ resampling backfill: при rotation создаётся entry в pulse_runtime.audit_log(operation='sampling.reshuffled', payload={generation, affected_event_types}) — analytics tooling уважает это как «обрыв» continuity для cross-generation comparisons.

Для ops/meta events с sample_strategy: random reshuffle irrelevant — там нет per-subject stickiness изначально.

10.7 Performance budgets для M0

Жёсткие SLO для M0. Превышение → автоматический PagerDuty/alert (если оркестрация подключена) и блокировка релиза в CI (если регрессия на нагрузочных тестах).

10.7.1 Ingest

МетрикаBudget M0
p50 latency /events/ingest (per batch ≤ 100)≤ 50 ms
p95 latency /events/ingest≤ 150 ms
p99 latency /events/ingest≤ 400 ms
Throughput на pulse-core ноду≥ 2 000 events/sec
Drop rate (по любым причинам кроме no-consent)≤ 0.01 %
Buffer-to-DB lag (p95)≤ 5 s

10.7.2 Identity-mapping

МетрикаBudget M0
p50 pseudonymize≤ 2 ms
p95 pseudonymize≤ 8 ms
p99 pseudonymize≤ 20 ms
p95 resolve_subject≤ 10 ms
Доступность (rolling 30-day)≥ 99.95 %

Identity-mapping — критический путь ingest; деградация ломает всю систему. При p99 > budget активируется circuit-breaker: pulse-core буферизует события и не пишет в БД до восстановления (≤ 60 s tolerance, дальше — degraded mode с явным alert).

10.7.3 Decision engine (rules)

МетрикаBudget M0
p50 evaluate-all-active-rules (per event)≤ 10 ms
p95 evaluate-all-active-rules≤ 30 ms
p99 evaluate-all-active-rules≤ 80 ms
Active rules supported (M0)до 200
DSL compile time (per rule, validation endpoint)≤ 200 ms
SMT validation per rule (M1+, не блокирует M0)≤ 5 s

10.7.4 GDPR endpoints

МетрикаBudget M0
p95 /gdpr/access (bundle generation)≤ 30 s
p95 /gdpr/erasure (acknowledgement)≤ 2 s
Erasure job completion (background)≤ 24 h
/gdpr/portability bundle≤ 60 s

Erasure-acknowledgement быстрый (job создан и поставлен в очередь); реальная батч-зачистка может идти до 24 h, что укладывается в регуляторные 30 дней.

10.7.5 Audit log

МетрикаBudget M0
p95 append-latency≤ 20 ms
Chain-verify скан 1M rows≤ 60 s
Storage growth (assuming 1k decisions/day)≤ 50 MB / month

Целостность hash-chain проверяется фоновым job'ом каждые 6 h на полном префиксе log; любая discrepancy → critical alert + freeze всех write-операций до investigation.

10.7.6 Resource budgets

Per pulse-core instance (M0 baseline, 2 vCPU / 4 GB RAM):

РесурсИдл/НормаПикАлёрт-порог
CPU≤ 20 %≤ 70 %> 85 % (5 min)
RAM≤ 1.5 GB≤ 3 GB> 3.5 GB (5 min)
Postgres connections≤ 20≤ 50> 60
Disk I/O (pulse_runtime)iowait > 30 %

10.7.7 Verification

Performance budgets верифицируются:

  • Load-tests в CI: k6/Locust сценарии (M1+); для M0 — manual baseline runs перед каждым release.
  • Производственный мониторинг: Prometheus метрики (pulse_ingest_latency_seconds, pulse_identity_mapping_latency_seconds, …) + Grafana dashboards.
  • Алёрты: AlertManager rules сгенерированы из этой таблицы автоматически (M1+); для M0 — ручная настройка.

Бюджеты пересматриваются на каждом minor-релизе spec (раз в 2-3 месяца) на основе production data.

10.8 Data retention policies

Retention за конкретными таблицами Pulse. Источники истины — retention_days в Telemetry contract (per-event-type) для raw events, плюс правила ниже для агрегатов и compliance-таблиц.

10.8.1 Per-table retention

ТаблицаRetentionОснование
pulse_runtime.eventsper-event retention_days (см. Telemetry contract)Минимально необходимый период. После — drop partition. По умолчанию: analytics=90d, ops=365d, billing-related=7y.
pulse_runtime.events_rejected30dDebug/drift detection; не нужен на дольше
pulse_runtime.feature_storeпока subject активенПерезаписывается, не растёт. При erasure-job — row deleted.
pulse_runtime.decisions730d (2 года)Replay, attribution, dispute resolution
pulse_runtime.action_acks730dСвязан с decisions
pulse_runtime.outbound_events365dProvider correlation, complaint investigation
pulse_runtime.audit_logforeverCompliance, Merkle chain integrity (см. §9.3, §10.6)
pulse_runtime.rule_stats365dRolling baseline для dead-rule detector
pulse_runtime.experiment_assignmentsдо 90d после ends_at соответствующего experimentSticky assignment нужен только пока эксперимент жив + grace period
pulse_runtime.federation_pointers90dDebug; реальный pointer state — в DHT
pulse_runtime.gdpr_jobs1825d (5 лет)Регуляторное требование на хранение compliance evidence
identity_mapping.access_audit1825dCompliance + security forensics
identity_mapping.subjectsпока не erasedПосле erasure — row deleted, остаются «висящие subject_id» в audit (см. §9.1.5)

10.8.2 Partition rotation для events

pulse_runtime.events партиционирована по месяцам (см. 03_pulse_runtime.sql). Rotation выполняется фоновым job'ом pulse-retention-worker (часть pulse-core; cron schedule в Nx config):

  1. Создание новой партиции. В 1-й день каждого месяца — CREATE TABLE events_YYYY_MM PARTITION OF events FOR VALUES FROM ... TO .... Идемпотентно через CREATE TABLE IF NOT EXISTS + проверку pg_partitions.
  2. Drop старых партиций. Партиция дропается когда все event-types, которые могли в неё попасть, превысили свой retention_days. На практике: если есть event с retention_days = 2555 — партиции хранятся 7 лет. Поэтому event-types с длинной retention пишутся в отдельную партиционированную таблицу events_long_retention (M1+; для M0 — один общий partitioning с max retention 365d, события с большей retention идут в отдельные таблицы events_billing).
  3. Vacuum / analyze. Стандартный Postgres autovacuum; явный manual VACUUM ANALYZE после drop partition.

10.8.3 Erasure interaction

Erasure-job (§9.1.5) удаляет данные раньше retention deadline:

  1. Identity-mapping помечает subject как erased.
  2. pulse-retention-worker в специальном «erasure mode» проходит по таблицам с user-data (events, decisions, outbound_events, feature_store) и удаляет rows с этим subject_id.
  3. Audit-log НЕ модифицируется — там остаётся subject_id как «висящий псевдоним», но без mapping обратимости (см. §9.1.5).
  4. Партиции events НЕ дропаются при erasure (partition содержит данные многих subjects); удаление — построчно (DELETE с индексом по subject_id).

SLA: полная erasure данных subject'а ≤ 24 ч (см. §10.7.4); compliance-окно регуляторов — 30 дней с момента запроса.

10.8.4 Backup retention

АртефактBackup frequencyBackup retentionEncryption
identity_mapping БДhourly + daily90dОтдельный ключ (ceremony output)
pulse_runtime.* + pulse_curated.*hourly + daily30dСтандартный кластерный ключ
pulse_runtime.audit_log (S3 cold)dailyforeverPer-period encryption keys

Restore-from-backup для identity-mapping требует двух операторов (key split). Для остальных таблиц — стандартная on-call процедура.

10.8.5 Compliance metadata

pulse_runtime.audit_log содержит дополнительные entries:

  • data.partition_dropped — каждый drop партиции записывается с (table, partition_name, dropped_at, rows_count).
  • data.erasure_executed — completion entries из gdpr_jobs.completed_at.

Это позволяет аудитору доказать, что Pulse действительно удалил данные в срок.

10.9 Canonical serialization для content hashing

Содержательный хеш контракта (contract_id = blake3(canonical_bytes)) должен совпадать бит в бит для любых валидных реализаций, независимо от языка и YAML-парсера. Это нужно для federation (несколько узлов сходятся на одинаковом hash для одного контракта) и для audit replay.

10.9.1 Pipeline

contract YAML file
        │  (1) parse YAML → typed object

domain model (Rust struct / TS object)
        │  (2) serialize → JSON

intermediate JSON
        │  (3) canonicalize (JCS / RFC 8785)

canonical JSON bytes (UTF-8)
        │  (4) blake3

contract_id := "b3:" || hex(32-byte digest)

10.9.2 Шаг (1) — YAML parsing rules

YAML — формат для людей, JSON — для канонизации. Чтобы избежать неоднозначностей:

  • Использовать YAML 1.2 (Core schema). Запрещены: aliases / anchors (& и *), merge keys (<<:), explicit tags (!!str). Если YAML-парсер находит — ошибка валидации (CI fails).
  • Числа: только integer и floating-point в decimal notation. Запрещены: hex, octal, binary, sexagesimal, .inf, .nan. Bool — только true/false.
  • Trailing newlines, indentation style — не значимы (нормализуются на шаге 2).

10.9.3 Шаг (2) — Serialize to JSON

  • Keys в objects — sorted lexicographically (UTF-8 code-point order). Это часть JCS, но явно подчёркиваем.
  • Optional/null fields — omit key, не пишем "key": null. Default values из meta-schema не материализуются — два контракта, идентичные модулей дефолтов, должны давать одинаковый hash.
  • Arrays — preserve order (порядок significant; например, purposes_required: [a, b][b, a] по семантике эволюции хотя для validator'а одинаковы).
  • Numbers: integer → без точки (42, не 42.0). Floats → RFC 8785 §3.2.2.3 (ES6 ToString form: 1.5, не 1.500).
  • Strings: UTF-8, без BOM. Escape sequences по RFC 8259 §7 (минимальные ", \, �-).

10.9.4 Шаг (3) — JCS (RFC 8785)

После serialize применяется JSON Canonicalization Scheme:

  • Sorted keys (уже done на шаге 2).
  • No insignificant whitespace.
  • Number formatting per ECMAScript ToString.
  • Reference implementation: crate serde_jcs (Rust), canonicalize (TS, npm).

10.9.5 Reference test vectors

В contracts/test-corpus/canonical/ лежат:

  • minimal_telemetry.yaml + minimal_telemetry.canonical.json + minimal_telemetry.blake3 (expected hash).
  • Аналогичные fixtures для action contract, semantic-layer, consent-purposes.

Тест: каждая реализация (pulse-core Rust, Directus extension TS, validate.py Python) должна produce точно совпадающие canonical bytes и blake3 hash. CI runs cross-language comparison.

10.9.6 Versioning

Если правила канонизации меняются (это редко, но возможно при обновлении JCS RFC), canonicalization_version: 2 field добавляется в meta-schemas. Контракты с разными canonicalization_version имеют разные content hashes даже при идентичном semantic содержимом — это feature, не bug (защита от silent drift).


11. Federation и согласование версий

11.1 Контракт как DHT-артефакт

Версии контрактов (Telemetry, Action, Semantic, Consent purposes) — иммутабельные artifact'ы, идентифицируемые content-hash. Артефакт публикуется в Kontinuum DHT по ключу:

contract-artifact:{contract_id}  →  { canonical_yaml_bundle, mime, size }

Артефакт может реплицироваться по обычному storage capability нод (см. docs/node/operations.md §7). Reference count поддерживается через активные channel pointers.

11.2 Channel pointers в DHT

Текущая активная версия канала — отдельная запись:

pulse-channel:{channel_name}  →  Signed{
    current_contract: blake3,
    previous_contract: blake3,
    effective_from: timestamp,
    expires: optional<timestamp>,
    signatures: [...],
    threshold: u32,
    monotonic_seq: u64
}

Pointer обновляется через стандартный DHT-write механизм с last-writer-wins по (monotonic_seq, signature_validity). Подписи проверяются по registry известных team-signing keys (см. §11.5).

11.3 Negotiation между узлами

Перед обменом telemetry или action-сообщениями между двумя нодами (или между client и Node) выполняется протокол согласования контракта:

A → B: ContractHello {
    contracts_supported: [
      { channel: "pulse/main", versions: [b3:7a2f...e91d, b3:5c1e...b04a] },
      { channel: "pulse/eu",   versions: [b3:91ab...44d3] }
    ]
}

B → A: ContractAck {
    selected: { channel: "pulse/main", version: b3:7a2f...e91d },
    fallback_offered: [b3:5c1e...b04a]
}

Если пересечение пусто на критическом канале, обмен либо deferred (одна из сторон обновится), либо degraded (только generic events без version-specific полей). Алгоритм выбора — наибольший общий monotonic_seq в пересечении.

11.4 Публикация audit-Merkle-корней в DHT

Каждый epoch (например, час или 10000 записей — выбирается в config) central формирует Merkle root батча audit-log и публикует:

pulse-audit-root:{epoch}  →  Signed{
    epoch: u64,
    range: { from_id: ulid, to_id: ulid },
    merkle_root: blake3,
    record_count: u64,
    contract_id: blake3,
    timestamp: timestamp,
    signature: ed25519
}

Узлы (Tier 0/1) могут периодически кэшировать эти roots для архивных целей; сторонний аудитор может скачать roots и сверить выборочные записи через Merkle-proof, получаемый у central через узкий API.

11.5 Signing keys и authority

Авторизация на изменение контрактов в production-каналах основана на multi-sig:

  • Tier 0 Anchor keys — публикация audit-roots, top-level governance.
  • Pulse team multisig (например, 2-of-3) — изменение pulse/main.
  • Channel-specific multisig — изменение subchannel'ов (например, pulse/eu).
  • Beta-deploy keys — пуш в pulse/beta, pulse/staging.

Registry ключей — отдельный artifact в DHT под pulse-keys-registry:current, обновляется реже остальных артефактов и сам подписан Tier 0.

11.6 Rollback и emergency stop

Откат к предыдущей версии — это публикация нового pointer'а с current_contract = предыдущая версия, явным флагом rollback: true и обязательным reason. Audit log фиксирует rollback-event отдельно.

Emergency stop (например, обнаружена утечка PII через новое event-поле): публикация current_contract: null со специальным флагом halt: true. Узлы, читающие этот pointer, останавливают ingest до восстановления.

11.7 Heterogeneous federation

Разные сегменты федерации могут жить на разных каналах:

  • pulse/main — стандартный.
  • pulse/eu — EU-сегмент, расширенный consent purposes, обязательные локализованные guardrails.
  • pulse/byo — для self-hosted PRO-узлов, ограниченный набор central-actions.

Каждая Node декларирует поддерживаемый канал в своей node:{node_id} записи (см. docs/node/protocols.md §5). Клиент при подключении к Node проверяет совместимость и при отсутствии общего канала отказывается отправлять telemetry, fallback'ясь на local-only режим.

11.8 Per-region regulatory overlay matrix

Архитектура Pulse одна для всех рынков (см. §3.6). Региональные требования реализуются как overlay — тонкая надстройка labels, consent purposes, regional metadata и UI-элементов поверх единого ядра, без изменения архитектурных invariants.

Эта секция фиксирует known overlay-требования по регионам. Каждое требование — это либо additional_consent_purpose, либо regional_metadata_field, либо regional_ui_artifact. Никаких архитектурных правок.

РегионOverlay-требованияStatus v0.7
EU (GDPR + AI Act + DSA + DMA)Formal /api/gdpr/access endpoint в machine-readable формате; consent UI с гранулярностью per purpose; DPO contact info в App Settings; AI risk assessment documentation для archetype dimension; sender address и legal entity address в каждом marketing email; Article 22 disclosure про auto-decisioning.M2
US — California (CCPA / CPRA)«Do Not Sell My Personal Information» link в App UI; right-to-know endpoint (близкий к GDPR access); опт-аут sensitive PI categories; CPRA-specific consent purposes (sensitive_pi_processing).M3+
US — federal (FTC, CAN-SPAM)Unsubscribe в каждом marketing email (уже invariant §17.4.2); truth-in-advertising compliance через composition constraints (уже §8.5.4); honest claims verification.M2 (уже)
Brazil (LGPD)Аналогично GDPR с национальными формулировками; DPO contact info на португальском; отдельный consent flow для transferência internacional de dados.M3+
UK (UK GDPR + PECR)Аналогично EU GDPR с UK ICO contact info; PECR-specific требования к marketing communications.M3+
Canada (PIPEDA + CASL)CASL-specific requirements для marketing emails (express consent, identification info); провинциальные variations (Quebec Law 25 ужесточает похоже на GDPR).M4+
Australia (Privacy Act + Spam Act)OAIC notification requirements; Spam Act-specific compliance для commercial messages.M4+
China (PIPL)Cross-border data transfer impact assessment; local data storage requirements; consent для sensitive personal information. Может быть architectural blocker для Kontinuum целиком из-за PIPL Article 41 (state security access). Open question.TBD
Russia (152-ФЗ)Local data storage requirements для personal data российских граждан. Может быть architectural blocker аналогично PIPL.TBD

Что архитектурно НЕ зависит от региона

Следующие свойства Pulse одинаковы для всех регионов и подтверждают тезис §3.6:

  • E2E-инвариант (no plaintext content).
  • Identity-mapping в отдельной БД с изолированным API.
  • Pseudonymization всех событий до ingest.
  • Archetype-billing compile-time block (template variants с archetype_affinity не могут менять side_effect класса billing_*).
  • Composition constraints в brand-guardrails (T16 mitigation).
  • Audit log с Merkle-цепочкой.
  • Three-engine separation (rules / ML / LLM).
  • Content-addressed contracts с signed pointers.
  • Consent-token validation на ingest (general consent infrastructure).

Что зависит от региона (overlay only)

Что меняетсяГде реализуетсяКак добавляется новый регион
Список consent purposespulse_curated.consent_purposes (региональный набор)Новая запись в коллекции
UI-элементы legal disclosureTemplates в pulse_curated.templates с regional variantsНовые templates, regional-routed
Mandatory fields в marketing emailBrand-guardrails class A constraints (региональные)Новые regional constraints, активируются по subject.country
Data storage locationFederation channel selection (pulse/eu, pulse/cn, ...)Новый federation channel со специфической топологией
Right-to-X endpointsExisting GDPR endpoints (access/erasure/portability)Региональная alias-маршрутизация на те же endpoints
Sensitive purpose categoriesMarking в Telemetry contract + Semantic LayerРегиональная аннотация на existing fields

Добавление нового региона

Шаги:

  1. Legal review: какие специфические требования (consent purposes, mandatory disclosures, data residency, retention).
  2. Consent purposes: добавить новые записи в pulse_curated.consent_purposes с regional scope.
  3. Templates: создать regional variants для marketing/transactional с обязательными disclosure-элементами.
  4. Brand-guardrails: добавить regional class A constraints (sender address, opt-out language, regional claims restrictions).
  5. Federation channel (если data residency требует): новый канал pulse/{region}, отдельный publishing pipeline, отдельные Tier 1 nodes в регионе.
  6. Documentation: обновить эту таблицу.

Не делается: изменение Pulse-core кода, изменение архитектурных invariants, добавление runtime conditional на country для core защит.


12. Модель угроз

12.1 Активы

АктивКонфиденциальностьЦелостностьДоступность
Identity ↔ subject mappingкритичнокритичноважно
Сырые E2E-данные пользователякритичнокритичноважно
Псевдонимизированный feature storeважноважноважно
Audit Logважнокритичноважно
Контракты в DHTнизко (публично)критичнокритично
Signing keysкритичнокритичноважно
ML-моделиважно (proprietary)важносредне

12.2 Угрозы и митигации

#УгрозаМитигация
T1Компрометация Pulse core → доступ к feature storeIdentity-mapping изолирован в отдельном сервисе/БД с отдельными ключами шифрования; PII-поля шифруются at rest.
T2Подмена контракта без авторизацииContent-addressing + multi-sig pointer + monotonic seq + previous_hash chain.
T3Rollback-атака на старую уязвимую версию контрактаprevious_hash chain + monotonic seq + explicit rollback: true flag with reason + audit-logging.
T4Freeze-атака (узел держат на старом pointer'е)Pointer имеет expires; узлы периодически re-fetch'ат; alerting при stale pointer'ах.
T5LLM exfiltration через prompt injectionPII-strip on input; tool catalog не содержит write-actions; audit логирует все обращения.
T6Подмена ack'а от action target → audit log liesAck подписан target'ом; audit хранит signature; mismatch обнаруживается reconciliation-jobом.
T7Telemetry-flood от скомпрометированного клиентаRate-limiting per (client_pubkey, event_type); edge Node может early-drop; ingest идемпотентен по event_id.
T8Утечка через timing / count side-channel в действиях LLMCost-aware caching на gateway; не-deterministic варианты ответов; не возвращать exact-count из gateway.
T9Корреляция subject_id ↔ identity внешним наблюдателемSalt rotation; rate-limit на identity-mapping API; нет публичных endpoint'ов в DHT с subject_id.
T10Утечка audit-Merkle позволяет восстановить activityMerkle root содержит только hash'и; полные записи доступны только central; root reveals только factum-existence.
T11Compromise одного signing-key для контрактовMulti-sig threshold (2-of-3+); key rotation; emergency revocation через Tier 0.
T12DSL injection через NL-rule-drafting (LLM выдала malicious правило)LLM никогда не публикует напрямую; SMT-валидатор проверяет правило; human approval перед publish.
T13Re-identification через cross-reference Feature StoreK-anonymity-проверка при экспорте агрегатов; DP-noise для cohort'ов <K subjects.
T14DoS на ingestRate-limit; backpressure из ingest в clients (signed); buffer на клиенте.
T15Регуляторное несоответствие (например, EU AI Act)Декларативные guardrails в LLM Gateway; audit class compliance_review_required (см. action-contract-schema.yaml); human-in-the-loop для critical decisions.
T16Manipulative composition patterns в outbound (fearmongering, ложная срочность)Brand-guardrails verifier с machine-checked composition constraints (см. §8.5.4 класс A+B); fearmongering_score ниже порога; audit-class brand_guardrail_violation для всех incident'ов; human approval для шаблонов класса outbound_communication первые N раз.

12.3 Out of scope

Pulse не пытается защититься от:

  • Атак на стороне источника событий (если клиент скомпрометирован, его телеметрия недостоверна — это видно по нерегулярности паттернов, но не предотвращается архитектурно).
  • Глобального network observer'а, наблюдающего, что subject передаёт telemetry в Pulse (это известный inherently leaked сигнал; mitigation — onion-routing через Tor — out of scope для v0.1).
  • Side-channels на стороне LLM-провайдера (мы не контролируем их инфраструктуру).

13. Интеграционные точки

13.1 С kontinuum-app

  • Telemetry emit: app эмитит события через telemetry SDK (Rust crate pulse-client). SDK берёт на себя batching, retry, consent-token signing, schema validation against contract.
  • Action receipt: app принимает action calls через App-side endpoint (см. §13.1.1 ниже).
  • Consent UI: app предоставляет настройку consent в Settings → Privacy. Изменения emit'ятся в Consent Store через специальный endpoint.
  • Decision explanation: app может запросить «почему мне показано это» → запрос в LLM Gateway → ответ subject'у.

13.1.1 App-side action endpoint

Pulse-core инициирует dispatch action'а к App через push-канал (для подключённого устройства) или через pull at startup (для offline-restored сессии). Формат payload фиксирован:

Endpoint: POST /api/pulse/action (внутри App; Tauri IPC или localhost HTTP).

Request body (JSON):

json
{
  "decision_id": "01JBZX0K9V0000000000000000",
  "action_type": "show_ui_banner",
  "issued_at": "2026-05-20T10:00:00Z",
  "expires_at": "2026-05-20T10:05:00Z",
  "parameters": { /* per action contract */ },
  "signature": "ed25519:..."
}
  • decision_id — ULID из pulse_runtime.decisions; App обязан сохранить его и включить во все события, derived от этого action'а (app.banner_shown.fields.decision_id, app.banner_clicked.fields.decision_id, и т.п.).
  • signature — Ed25519 over canonical(payload − signature field) с pulse-core's signing key. App verify'ит подпись против embedded pulse-core public key.
  • expires_at — если App не успел отрисовать до этого момента (например, был offline), action drop'ается локально с emission pulse_internal.client_event_dropped(drop_reason=action_expired).

Response (App → pulse-core, async через /actions/ack endpoint pulse-core):

json
{
  "decision_id": "01JBZX0K9V0000000000000000",
  "status": "success | rejected | deferred",
  "reason": "optional human-readable",
  "completed_at": "2026-05-20T10:00:03Z"
}

Идемпотентность: повторный приход action'а с тем же decision_id обрабатывается App'ом как no-op (App кэширует обработанные decision_ids на TTL=24h).

Connection:

  • Подключённое устройство: WebSocket / SSE поверх mTLS (М2+; M0 — long polling каждые 60s или push при foreground).
  • Offline-restore: App при startup'е делает GET /api/actions/pending?since={last_ack_at} — pulse-core возвращает queue not-yet-ack'нутых dispatches.

13.2 С kontinuum-veil / kontinuum-tether / kontinuum-send

Эмитят telemetry через тот же SDK. Поскольку у них меньше UI-логики, action-обработчики у них специализированы:

  • Veil: set_relay_policy, recommend_relay_node.
  • Tether: recommend_battery_mode, notify_pairing_risk.
  • Send: mute_recipient (если signals fraud).

13.3 С kontinuum-node

  • DHT distribution: контракты, channel pointers, audit-roots, keys-registry публикуются в DHT через стандартный storage capability.
  • Telemetry relay: edge Node (если Tier 1 owner-tenant) может выступать как relay для telemetry своих attached subjects (буферизация + forward в central). Это снижает зависимость клиента от прямого соединения с central.
  • Federation-policy DSL runtime: Node-сторонний компонент pulse-edge (компилируется в Node-бинарь как опциональный feature) исполняет federation-уровневые правила.

13.4 С billing (Directus)

Pulse и Billing архитектурно объединены через единый admin UI и shared Postgres, при разделённом backend-runtime (см. §4.4-bis).

Shared Postgres layout (см. также §10.4):

СхемаOwnerНазначениеДоступ Directus admin
billingDirectusCustomers, subscriptions, payments, plans, capacityfull (как сейчас)
pulse_curatedDirectusRules, templates, experiments, consent purposes, approval queue, segmentsfull через стандартные коллекции
pulse_runtimePulse-coreEvents, features, decisions, auditread-only через views с RLS
identity_mapping(отдельныйidentity ↔ subject (см. §9.1)нет доступа
инстанс)

Поток данных:

  • Subscription events (Directus → Pulse): hook'и Directus при изменении подписки эмитят события в Pulse telemetry contract (billing.subscription_started, billing.payment_failed, billing.plan_changed). Pulse-core потребляет их как обычные telemetry-входы.
  • Action sink (Pulse → Directus): billing-actions из Action Contract (grant_relay_credits, set_pricing_tier и т.п.) применяются Directus-хуками. Hook верифицирует decision_id, проверяет idempotency, выполняет изменение state, возвращает ack в audit log.
  • Consent storage: Consent Store физически в billing или pulse_curated (pulse_curated.consent_grants). Pulse-core читает через узкий read API; Directus admin UI редактирует через стандартные коллекции с обязательным audit-логированием.
  • Curated artifacts (Directus ↔ Pulse): rules, templates, experiments — Directus admin создаёт/редактирует записи в pulse_curated.*. Pulse-core при старте/hot-reload подхватывает изменения. Compile-проверки и SMT-валидация — server-side hook через pulse-core API (POST /api/dsl/validate).

UI поверх этого:

  • M0: всё через стандартные Directus коллекции, без custom-кода.
  • M1+: специфичные поля получают custom interfaces (DSL editor, metric/action pickers).
  • M2+: сложные сценарии становятся custom modules внутри того же Directus admin (Rule Studio, Experiments Console, Audit Explorer, Pulse Chat).

Идентификационная служба (identity-mapping) намеренно не интегрирована в Directus: она остаётся отдельной БД с собственным узким API. Это сохраняет privacy invariant (см. §9.1) даже при компрометации Directus admin'а.

13.5 С marketing infrastructure

  • Email / push providers: исходящие action типа send_promo_email передаются через провайдер (SendGrid / Postmark / собственный SMTP) с verified domain, DKIM/SPF, и обязательным unsubscribe link, генерируемым с decision_id.
  • External experiments tools: интеграция с GrowthBook / Eppo — опциональная, через адаптер. По умолчанию используется self-hosted experiment runtime (см. §8.4).

13.6 С observability stack

  • Технические метрики pulse-core экспортируются в существующий Prometheus + Grafana (см. docs/internals/observability.md).
  • Бизнес-метрики и dashboards Pulse — отдельные, поверх Feature Store / Audit Log; могут использовать тот же Grafana как универсальное UI, но через PostgreSQL-datasource (не Prometheus).
  • Loki — для логов сервисов pulse-*.

13.7 Client SDK contract

Каждый клиентский продукт (App, Veil, Tether, Send) эмитит события в Pulse через общую клиентскую библиотеку, не катит свой клиент HTTP-запросов. Это даёт единую buffer/retry/consent-token-refresh логику и предотвращает дрейф между источниками.

13.7.1 Crate / package layout

ПродуктЗависимостьЯзык
kontinuum-app (Tauri host)pulse-client (Rust crate, в kontinuum-pulse/crates/client/)Rust
kontinuum-app (webview)@kontinuum/pulse-client (TS, тонкая обёртка над Tauri IPC)TS
kontinuum-veilpulse-client (Rust)Rust
kontinuum-tether (Android)pulse-client-android (Kotlin wrapper над Rust через JNI)Kotlin/Rust
kontinuum-send (CLI)pulse-client (Rust)Rust
Directus backend / billing@kontinuum/pulse-client-node (TS, server-side через mTLS, source=billing)TS

Rust crate — каноническая реализация; TS и Kotlin — тонкие FFI/Tauri обёртки. Все три собираются в одном kontinuum-pulse/ workspace.

13.7.2 API surface

Минимальный публичный API клиентского крейта (стабилизирован для M0):

rust
pub struct PulseClient { /* ... */ }

impl PulseClient {
    pub fn new(config: PulseClientConfig) -> Result<Self>;
    pub fn emit(&self, event_type: &str, fields: serde_cbor::Value) -> Result<()>;
    pub async fn flush(&self) -> Result<FlushReport>;
    pub fn shutdown(&self) -> Result<()>;
}

pub struct PulseClientConfig {
    pub endpoint: Url,                       // pulse-core ingest URL
    pub source: EventSource,                 // client | edge | billing
    pub buffer_path: PathBuf,                // on-disk buffer for offline mode
    pub buffer_max_bytes: u64,               // default 50 MiB
    pub max_batch_size: usize,               // default 100
    pub flush_interval: Duration,            // default 30 s
    pub mtls_identity: Option<MtlsIdentity>, // server-to-server callers
    pub consent_provider: Arc<dyn ConsentTokenProvider>, // platform-specific
}

emit неблокирующий: кладёт событие в in-memory ring buffer (≤10k events). При переполнении spill на диск в buffer_path (LMDB или sled). Failure-mode: drop oldest + warn metric.

13.7.3 Buffer / retry semantics

  • Batching. Flush triggers: (1) max_batch_size events accumulated, (2) flush_interval elapsed, (3) explicit flush() call, (4) graceful shutdown.
  • Persistence. Events на диске survive process restart. Buffer rotation: при достижении buffer_max_bytes — drop oldest (FIFO).
  • Retry policy. Exponential backoff с jitter: base 1 s, max 5 min, factor 2.
    • 5xx / network error → retry.
    • 429 → retry с Retry-After honoring (max 5 min).
    • 400 / 401 (consent expired / malformed) → НЕ retry; drop event + emit pulse_internal.client_event_dropped (если квоты позволяют).
    • 2xx / 207 → ack; для 207 — обработать per-event status в response (results[]).
  • Backpressure. При непрерывных 429 на ≥5 минут клиент переходит в degraded mode: уменьшает emit rate в 2x и отображает warning в pulse_internal.client_degraded для центрального мониторинга.

13.7.4 Transport

  • Protocol. HTTP/1.1 минимум, HTTP/2 предпочтительнее. WebSocket / streaming — M2+.
  • mTLS. Обязателен для source ∈ {edge, billing, central}. Для source = client — TLS 1.3 без client-cert; auth через consent_token.
  • Compression. Content-Encoding: zstd (fallback gzip). Бюджет на размер batch'а после компрессии: ≤256 KiB (защита от runaway batches).
  • Endpoint. POST {endpoint}/api/events/ingest (см. contracts/openapi.yaml).

13.7.5 Schema validation

Опциональна на client-side. По умолчанию — off для prod (latency); on для debug/staging builds. Validator читает Telemetry contract из embedded copy (синхронизированной с central через version endpoint при старте). При schema_hash mismatch с central — client warning, но события всё равно отправляются (ingest решает финально).

Клиент НЕ хранит сам consent state — это сложно поддерживать корректно. Вместо этого:

  • ConsentTokenProvider — platform-specific trait, который SDK дёргает перед каждым flush'ем.
  • Provider возвращает HashMap<PurposeId, ConsentToken> действующих токенов.
  • SDK прикладывает соответствующий токен к каждому событию по его purpose (из embedded telemetry contract).
  • Если provider не может выдать токен для purpose (нет consent'а) — событие drop'ается в SDK с emission pulse_internal.client_event_dropped(reason = consent_missing).

Это значит: клиент-side у нас всё-таки минимальная копия consent state (in-memory cache, обновляется при app.consent_changed), но точное хранилище — Stronghold / Keychain, owned by App, не SDK.

13.7.7 Privacy invariants client-side

  • SDK НИКОГДА не логирует payload events в general logger (только в structured Pulse-собственный debug log при RUST_LOG=pulse_client=trace).
  • SDK НИКОГДА не отправляет события в crash reporter (Sentry / Crashlytics).
  • При panic в SDK — drop in-memory buffer, не flush на диск (защита от утечки PII через crash dumps).

13.7.8 Versioning

Crate pulse-client имеет независимый semver. Совместимость с pulse-core:

  • Major bump — breaking API; требует перекомпиляции consumer'ов.
  • Minor bump — добавление событий / полей в embedded contracts.
  • Patch — bugfixes.

Embedded Telemetry contract в crate ≥ active central contract. Несовпадение приводит к warning в version endpoint check на startup, но не блокирует работу — central фильтрует unknown event_types.


14. Версионирование и миграции

14.1 Версионирование артефактов

АртефактВерсия
Telemetry contractcontract_id (blake3) + semver tag
Action contractcontract_id (blake3) + semver tag
Semantic Layercontract_id (blake3) + semver tag
Consent purposes registrycontract_id (blake3) + semver tag
Rules Registryper-rule R0042.v3; bundle has own contract_id
ML modelsmodel_name@version; артефакты в отдельном storage (S3 / Kontinuum DHT)
LLM promptsprompt_id@version; артефакты иммутабельны
Audit Logepoch идентификатор партии

14.2 Совместимость

При смене активного pulse/main контракта Pulse-core поддерживает multi-version live read:

  • Принимает события с schema_hash старой или новой версии (пока обе находятся в effective window).
  • Парсит по соответствующим compiled types.
  • В Feature Store записывает в общем формате (через миграцию полей по карте rename'ов).

При changes уровня breaking Pulse-core останавливает приём, пока operator явно не пройдёт через migration script. Это намеренная защита от тихих несоответствий.

14.3 Миграции схем

Для каждого breaking-изменения публикуется отдельный artifact:

yaml
migration:
  from_contract: b3:5c1e...b04a
  to_contract: b3:7a2f...e91d
  steps:
    - rename_field: { event: veil.session_ended, from: bytes_total, to: bytes_in }
    - split_field:  { event: veil.session_ended, from: bytes_total, to: [bytes_in, bytes_out], formula: "..." }
    - drop_field:   { event: legacy.foo, field: bar }
  reversible: false

Migration scripts применяются Pulse-trainer'ом при backfill'е offline store и pulse-core при reading исторических событий.

14.4 Lifecycle модели данных

   sensitive raw → consent-filter → ingest → feature-store (live)


                                          ML training input

                                          retention expires


                                          hash-only retention

Audit Log записи живут дольше, чем сами данные, но содержат только hash-references.


15. Roadmap

Полный milestone-роадмап Pulse (M0 Foundation … M5+ Sovereignty), стратегия эволюции UI (бывш. §15-bis) и meta-automation strategy (бывш. §15-ter) вынесены в GitLab — см. issue #45. Текущее реализованное состояние M0 — companion-документ implementation-status.md.

16. Открытые вопросы

Открытые и отложенные архитектурные вопросы (Q-серия: family-mode, self-hosted Tier 2 feature store, ML model distribution, LLM cost attribution, legacy bridge, composition-constraint severity, pattern-effectiveness, и pre-prod lawyer-review по audit-log hash-references) вынесены в GitLab — см. issue #45. Решённые (CLOSED) вопросы зафиксированы как решения в своих профильных секциях спецификации (идентичность субъекта — §9.1.1/DDL, SMT-движок Z3 — dsl-grammar.md §9, latency-бюджеты — §10.7.3, experiment-assignment salt — §10.6.6/DDL, archetype как soft-preference — §9.2.6).

17. Marketing channels integration

17.1 Назначение

Pulse — оркестратор и аналитик, не сам канал. Action contract (§7) описывает типизированные действия, которые Pulse предлагает применить; физически отправляют письмо/push/баннер/in-app сообщение — конкретные каналы-исполнители (внутри Kontinuum или внешние провайдеры). Эта секция описывает каталог этих каналов, их связь с Action Contract и Telemetry Contract, граничную ответственность и порядок включения в roadmap.

Связь с существующим маркетинговым планированием: 26 субпродуктов по 6 категориям (Programs, Content, Social, Brand, Interactions, PR&Launch) отслеживаются как план в GitLab (umbrella #43). Pulse даёт техническую инфраструктуру для оркестрации этих субпродуктов; сами субпродукты — отдельные единицы маркетингового плана.

17.2 Классификация каналов

КлассЧто этоPulse-связьPrivacy-suitability для Kontinuum
In-productBanners, modals, onboarding, inbox, surveys внутри AppСильная: actions + telemetry прямоИдеальная
Direct outboundEmail, push, in-app messages с consentСильная: actions через провайдеровХорошая, при строгом consent
Content inboundMarketing site, blog, tutorials, SEOСлабая: только first-touchИдеальная
Community-ledDiscord, Matrix, GitHub, ambassador, referralСредняя: opt-in linking + referralИдеальная
PR / launchesProductHunt, Show HN, awesome-listsСлабая: retrospective атрибуцияХорошая для запусков
Paid adsGoogle Ads, social adsСлабая: UTM-onlyСложная (несовместима с трекерами)

«Pulse-связь» — степень, в которой канал управляется Pulse через Action Contract и эмитит обратно telemetry. «Сильная» = двусторонняя петля (action + ack + engagement events). «Слабая» = только post-fact attribution через UTM или referrer.

17.3 In-product channels

17.3.1 UI Banners

Где живёт: компонент <PulseBanner> в App/Veil/Tether/Send. Подписан на канал banner-actions от Pulse-core через Tauri IPC / HTTP shim.

Action Contract:

yaml
show_ui_banner:
  target: app
  parameters:
    banner_id: string
    template_id: string
    template_params: map<string, string>
    placement: enum[top, modal, sidebar, settings_inline, onboarding_step]
    expires_at: optional<timestamp>
  side_effect: ui_state
  idempotency_key: "{subject_id}:{banner_id}"
  requires_consent: personalization
  audit_class: ui_personalization

Telemetry:

  • app.banner.shown { banner_id, decision_id, placement }
  • app.banner.clicked { banner_id, decision_id, cta }
  • app.banner.dismissed { banner_id, decision_id, reason }

Pulse-роль: правило типа «при определённом контексте показать banner X»; experiment над вариантами текста/CTA; frequency cap.

17.3.2 Onboarding flows

Где живёт: multi-step wizard / modal в App. Конфигурируется через pulse_curated.onboarding_flows (steps, branching, completion criteria).

Action Contract:

yaml
start_onboarding_flow:
  target: app
  parameters:
    flow_id: string
    start_step: optional<u8>
    skip_completed_steps: bool
  side_effect: ui_state
  idempotency_key: "{subject_id}:{flow_id}"
  requires_consent: personalization

Telemetry:

  • app.onboarding.flow_started { flow_id }
  • app.onboarding.step_shown { flow_id, step }
  • app.onboarding.step_completed { flow_id, step }
  • app.onboarding.completed { flow_id, total_duration_ms }
  • app.onboarding.abandoned { flow_id, last_step }

Pulse-роль: start triggering (при первом запуске / при достижении milestone'а / при тарифном апгрейде); A/B тест порядка шагов; auto-funnel analytics.

17.3.3 In-app inbox

Persistent notification center внутри App. Альтернатива push для пользователей без push-consent; не теряется при оффлайне.

Action Contract:

yaml
post_to_inbox:
  target: app
  parameters:
    subject_id: subject
    message_template: string
    template_params: map<string, string>
    priority: enum[low, normal, high]
    expires_at: optional<timestamp>
    actions: list<{ label: string, deeplink: string }>
  side_effect: ui_state
  idempotency_key: "{subject_id}:{message_template}:{day}"
  requires_consent: personalization

Telemetry:

  • app.inbox.delivered { message_id, decision_id }
  • app.inbox.read { message_id, decision_id }
  • app.inbox.action_clicked { message_id, decision_id, action_label }
  • app.inbox.dismissed { message_id, decision_id }

Pulse-роль: routing «через какой канал отправить» — если push-consent отсутствует, fallback в inbox; если оба разрешены — выбор по приоритету и истории engagement.

17.3.4 In-app surveys

Запланировано в роадмапе (см. issue #45). Action show_survey, события survey.shown/responded/dismissed. Ответы становятся первоклассными фичами в Feature Store.

17.3.5 Local notifications

OS-уровневые уведомления, запускаемые локально приложением (без серверного push). Полезны для re-engagement subjects, отказавшихся от push-consent, но согласных на local-нотифы (например, «вы не открывали App неделю»).

Action Contract:

yaml
schedule_local_notification:
  target: app
  parameters:
    subject_id: subject
    notification_template: string
    template_params: map<string, string>
    deliver_at: timestamp  # абсолютное или relative
    can_user_cancel: bool
  side_effect: ui_state
  requires_consent: local_notifications  # отдельный purpose

Telemetry: app.local_notification.scheduled / delivered / opened / cancelled.

17.4 Direct outbound channels

17.4.1 Transactional email

«Ваш payment failed», «recovery phrase сохранён», «новый device paired», «security alert». Считается operational, не маркетинговым.

Где живёт: SMTP-провайдер (SendGrid / Postmark / собственный с DKIM). Pulse-core делает HTTP-вызов через provider adapter (§17.9).

Action Contract:

yaml
send_transactional_email:
  target: external_provider
  parameters:
    subject_id: subject
    template_id: string
    template_params: map<string, string>
    priority: enum[normal, high, critical]
  side_effect: outbound_message
  idempotency_key: "{subject_id}:{template_id}:{trigger_event_id}"
  requires_consent: ops  # operational, не marketing
  audit_class: outbound_communication

Telemetry (через webhook от провайдера):

  • email.queued { message_id, decision_id }
  • email.delivered { message_id, decision_id, provider_id }
  • email.bounced { message_id, decision_id, bounce_reason }
  • email.complained { message_id, decision_id } — спам-жалоба

email.opened и email.clicked для transactional — opt-in отдельно, потому что pixel-tracking противоречит общему этосу Kontinuum. По умолчанию open-tracking выключен для transactional.

17.4.2 Marketing email

То же, что transactional, но:

  • requires_consent: marketing.email.
  • Обязательный unsubscribe link, генерируемый из decision_id.
  • Rate-limit per subject жёстче.
  • Open-tracking разрешён только при отдельном opt-in (marketing.email_engagement_tracking).

Action Contract: send_marketing_email (см. §7.2 в спеке — это пример).

Pulse-роль: сегментация, scheduling, frequency cap, A/B тест шаблонов и subject line'ов.

17.4.3 Push notifications

Где живёт: OS-native push.

  • iOS — APNS, через собственную инфру (Pulse-core держит провайдерский cert) или через minimum-data-leak relay.
  • Android — FCM (Google) или unified push (для users без Google Services).
  • Web push (PWA) — VAPID-protocol через собственный сервер.

Action Contract:

yaml
send_push:
  target: external_provider
  parameters:
    subject_id: subject
    template_id: string
    template_params: map<string, string>
    deeplink: optional<string>
    silent: bool  # data-only без user-visible notification
    priority: enum[normal, high]
  side_effect: outbound_message
  idempotency_key: "{subject_id}:{template_id}:{day}"
  requires_consent: marketing.push  # для visible; ops для silent
  audit_class: outbound_communication
  rate_limit:
    per_subject: { count: 3, window: "24h" }

Telemetry: app.push.received / opened / dismissed.

Privacy nuance: push-токен — это PII (привязан к device + service account). Mapping push-token ↔ subject держится в identity-mapping service (см. §9.1), не в обычном feature store.

17.5 Content & inbound channels

17.5.1 Marketing site

Отдельный статический сайт (kontinuum.* / лендинги для субпродуктов, см. план в GitLab #43). Не интегрирован с Pulse напрямую.

Где живёт: статический генератор (Astro / Next / VitePress), деплой независимо от App.

Analytics: отдельный privacy-friendly инструмент:

  • Plausible (рекомендуется) — без cookies, без cross-site tracking, GDPR-friendly out of the box.
  • PostHog Cloud — если нужны session replay лендинга и расширенная воронка; только для сайта, не для App (см. ранее).
  • Self-hosted Umami — альтернатива Plausible.

Pulse эти данные не видит. Связь — через §17.5.2.

17.5.2 First-touch attribution

Единственная phantom-связь pre-install и post-install событий — через UTM-параметры в install context.

Поток:

  1. Маркетинговая ссылка содержит UTM-параметры: https://kontinuum.app/download?utm_source=hn&utm_medium=post&utm_campaign=launch_v2.
  2. Сайт сохраняет UTM в localStorage (cookie-free) перед download'ом.
  3. Installer / first-launch flow читает локальную capture и эмитит:
app.installed {
  utm_source: "hn",
  utm_medium: "post",
  utm_campaign: "launch_v2",
  referrer_domain: "news.ycombinator.com",
  install_timestamp: ...,
  app_version: ...
}
  1. Pulse записывает это как первое событие subject'а; UTM становятся dimension'ами в Semantic Layer:
yaml
dimension: attribution.utm_source     # type: string, low_cardinality
dimension: attribution.utm_medium
dimension: attribution.utm_campaign
dimension: attribution.referrer_domain

Что НЕ делается:

  • Никакого device fingerprinting (canvas, fonts, WebGL).
  • Никакого cross-domain tracking pixel'ов.
  • Никакого IP-based привязки между сайтом и App.

UTM — это self-reported атрибуция; пользователь явно прошёл через эту ссылку.

17.5.3 Blog / tutorials / SEO content

Часть marketing site (см. 17.5.1). С Pulse связан только через §17.5.2.

Каждый контент-юнит может иметь свой utm_campaign для аналитики «сколько installs дал этот туториал».

17.6 Community-led channels

17.6.1 Discord / Matrix / GitHub

Из subproducts: «14 GitHub Community», «15 Discord/Matrix».

Где живёт: внешние платформы.

Связь с Pulse — слабая по дефолту. Пользователи communities анонимны для Pulse.

Opt-in linking: пользователь может в App связать свой Kontinuum identity с GitHub/Discord handle через OAuth-pairing (требует отдельный consent purpose external_identity_linking). Тогда:

  • Webhook'и от Discord/Matrix при определённых событиях (вступление, активность, milestone roles) → ingest в Pulse как events community.discord.*.
  • Cross-platform engagement становится dimension в Feature Store.

Action Contract: Pulse обычно не пишет в communities. Исключение — moderation actions для ambassador-программы, но только в pre-approved scope.

17.6.2 Referral program

Самый ценный community-channel для privacy-продукта — word-of-mouth с экономическим стимулом.

Где живёт: часть App. Subject генерирует referral_code, делится. Новый user при установке вводит код → ассоциация в identity-mapping.

Action Contract:

yaml
generate_referral_code:
  target: app
  parameters:
    subject_id: subject
    expires_at: optional<timestamp>
  side_effect: ui_state
  requires_consent: personalization

show_referral_card:
  target: app
  parameters:
    subject_id: subject
    placement: enum[settings, post_action_modal, share_panel]
  side_effect: ui_state
  requires_consent: personalization

grant_referral_reward:
  target: billing
  parameters:
    referrer_subject_id: subject
    referred_subject_id: subject
    reward_type: enum[bonus_gb, free_month, credits, pro_trial]
    reward_amount: u32
    reason: string
  side_effect: billing_state_change
  audit_class: billing_credit
  requires_human_approval: false  # auto, если в pre-approved границах

Telemetry:

  • app.referral.code_generated { code_hash } — сам код не пишется, только hash для dedup.
  • app.referral.code_used { code_hash, referrer_subject_id_hash, referred_subject_id } — связь устанавливается через identity-mapping.
  • app.referral.reward_granted { referrer, referred, reward_type, reward_amount }.

Pulse-роль:

  • Решает, когда показать referral card (rule: «active 14d AND no_referrals → suggest»).
  • Fraud detection: подозрительные паттерны (массовая регистрация с одного referrer'а в коротком окне, low-activity referred subjects).
  • Optimal reward amount: A/B тест («1 GB vs 5 GB free → conversion uplift»).

Privacy nuance: referral создаёт graph между subjects, что само по себе является PII. Mapping держится в identity-mapping service; в feature store доступен только агрегат «у subject X есть N успешных referrals».

17.6.3 Ambassador program

Из subproducts: «13 Ambassador».

Где живёт: часть App (settings → ambassador panel) + внешние тулы для трекинга активности (контент-публикаций, проведённых митапов и т.п.).

Action Contract:

yaml
invite_to_ambassador:
  target: app
  parameters:
    subject_id: subject
    program_tier: enum[contributor, evangelist, partner]
  requires_human_approval: true

grant_ambassador_reward:
  target: billing
  parameters:
    subject_id: subject
    reward_type: enum[pro_lifetime, swag_credit, conference_pass, custom]
    reward_value: u32
  audit_class: billing_credit
  requires_human_approval: true  # всегда human для ambassador

Telemetry: app.ambassador.* события + ручной ввод активности через Directus admin.

17.7 PR & launches channels

Из subproducts: «23 Product Hunt», «24 Show HN», «25 Awesome Lists», «26 Made with Badge».

Где живёт: разовые/периодические события на внешних платформах. Action'ов Pulse сюда не шлёт (нет API типа «опубликовать на ProductHunt»).

Связь с Pulse:

  • First-touch attribution post-install через UTM (§17.5.2).
  • Pulse эмитит retrospective insight'ы: launch'и помечаются в pulse_curated.launch_events (start/end даты, целевой канал); Insights показывают conversion / retention в когортах по дате install относительно launch date.

Action Contract:

yaml
mark_launch_event:
  target: pulse  # meta-уровень, не пользовательский
  parameters:
    launch_id: string
    channel: enum[product_hunt, show_hn, reddit, blog_post, other]
    start: timestamp
    end: optional<timestamp>
    expected_traffic: optional<u32>
  side_effect: none
  audit_class: meta

Это annotation в pulse_curated.launch_events, не действие в attribute-sense.

17.8 Paid ads

Низкий приоритет до proven LTV/CAC. Если используется:

Networks:

  • Google Ads / Bing Ads — да, через UTM only.
  • Twitter/X, Reddit ads — да, UTM only.
  • Facebook/Meta — несовместимо с brand'ом Kontinuum (требует Meta-pixel).
  • Privacy-friendly: EthicalAds, CarbonAds — естественнее для нашей аудитории.

Что НЕ делается:

  • Conversion pixel на client side App.
  • Re-targeting через third-party trackers.
  • Lookalike audiences на основе email/phone hash.

Что делается:

  • UTM-only first-touch attribution (как §17.5.2).
  • Server-side conversion reporting через privacy-preserving APIs (Google Enhanced Conversions через hashed identifier — только если пользователь дал consent на marketing).
  • Budget optimization вручную через external admin (Google Ads UI), Pulse даёт retrospective ROI-аналитику через attribution.utm_source в feature store.

17.9 Provider adapter architecture

Внешние провайдеры (SMTP, push, webhook ingestion) подключаются через унифицированный adapter в pulse-core.

Структура adapter'а:

rust
trait OutboundProviderAdapter {
    fn capabilities(&self) -> ProviderCapabilities;  // email/push/sms, etc.
    fn send(&self, message: OutboundMessage) -> Future<Result<ProviderAckId>>;
    fn webhook_endpoint(&self) -> Option<HttpHandler>;  // для receipts
}

Конфигурация в pulse_curated.outbound_providers:

yaml
- id: smtp_main
  type: smtp_sendgrid
  capabilities: [email_transactional, email_marketing]
  credentials_ref: stronghold:sendgrid_api_key  # ссылка, не plaintext
  webhook_path: /webhooks/sendgrid
  rate_limit: { per_second: 100, daily: 100000 }
  active: true

- id: push_apns
  type: apns
  capabilities: [push_ios]
  credentials_ref: stronghold:apns_cert
  webhook_path: null

Зачем: провайдеры заменяемые. Если SendGrid уходит — меняется одна запись + adapter, остальная инфраструктура продолжает работать. Шаблоны и решения — на стороне Pulse, провайдер используется как тупой relay.

Webhook ingestion: делать с верификацией подписи провайдера (HMAC), валидацией idempotency через provider_message_id, маппингом обратно в decision_id (хранится в hidden header при отправке).

17.10 Attribution model

Тип атрибуцииЧто отслеживаетсяГде применяется
First-touch (UTM)utm_source/medium/campaign/content/term + referrer_domainПресс-релиз, launches, ads, content marketing
In-product touchpointКакие banners/onboarding/emails subject виделMulti-touch для product-led growth
Referral chainКто кого invite-ил (subject → subject)Word-of-mouth, ambassador
Self-reportedOnboarding survey «как вы узнали о Kontinuum?»Качественная атрибуция, complement UTM

Multi-touch attribution model в M3+: Стандартные модели — last-touch, first-touch, linear, time-decay, position-based — реализуются как derived features в Semantic Layer над audit log и telemetry. Маркетолог выбирает модель в Insights-панелях; «истина» — first-touch (физическая) + multi-touch (вычислимая).

17.11 Channel × Action × Telemetry inventory

ChannelActionKey telemetryMilestone
UI Bannershow_ui_bannerapp.banner.shown/clicked/dismissedM0
Onboarding flowstart_onboarding_flowapp.onboarding.*M1
In-app inboxpost_to_inboxapp.inbox.delivered/read/clicked/dismissedM2
Surveyshow_surveysurvey.shown/responded/dismissedM2
Local notificationschedule_local_notificationapp.local_notification.*M2
Transactional emailsend_transactional_emailemail.delivered/bounced/complainedM1
Marketing emailsend_marketing_emailemail.opened/clicked (opt-in)M2
Push notificationsend_pushapp.push.received/openedM2
First-touch attribution(passive) app.installedUTM dimensions в Feature StoreM1
Referralgenerate_referral_code, grant_referral_rewardapp.referral.*M3
Discord/Matrix linkinglink_external_identitycommunity.discord.* (opt-in)M4
Ambassadorinvite_to_ambassador, grant_ambassador_rewardapp.ambassador.*M4
PR launch markermark_launch_event (meta)n/aM3
Paid ads attribution(passive) расширение app.installedUTM/server-side conversion reportingM5+

Конкретные семвер-версии Action contract публикуют этот inventory как часть actions.yaml.

17.12 Граница ответственности Pulse vs канал

ЗадачаPulseКанал-исполнитель
Решение «кому/когда/что отправить»
Сегментация аудитории
A/B варианты
Frequency cap, consent проверка(повторно)
Рендеринг template → финальный текст
Физическая доставка
Deliverability мониторинг
Логирование dispatch в audit
Ack от канала(отправляет ack)
Engagement events (open, click)✓ (emit webhook)
Превращение webhook в telemetry
Attribution в Semantic Layer

Рендеринг шаблонов: хранится в pulse_curated.templates (текст с placeholders). Pulse-core рендерит до отправки, провайдер получает финальный текст. Это даёт:

  • Контроль шаблонов в Directus admin без обращения к провайдеру.
  • Reproducibility: audit log хранит template_id@version + params_hash; рендер можно воспроизвести.
  • Independence от lock-in: смена провайдера не требует переноса шаблонов.

17.13 Privacy considerations для каналов

Consent matrix:

КаналRequired consent purposeNotes
UI bannerpersonalizationМожно работать с уменьшенной частотой без consent
OnboardingpersonalizationБазовый onboarding допустим без, расширенный — с
InboxpersonalizationCritical operational alerts — ops
SurveypersonalizationAnonymous surveys — ops
Local notificationlocal_notificationsОтдельный purpose
Transactional emailopsОбязательный для product operation
Marketing emailmarketing.emailOpt-in only, unsubscribe в каждом письме
Email open trackingmarketing.email_engagement_trackingОтдельный sub-purpose
Pushmarketing.push / opsVisible vs silent
Discord/Matrix linkexternal_identity_linkingНовый purpose
Paid ads attributionmarketing.attributionServer-side privacy-preserving conversion only

Что НЕ автоматизируется (всегда human):

  • Создание нового шаблона marketing-сообщения (legal/compliance review).
  • Изменение consent purposes (юридический документ).
  • Подключение нового external provider (security review).
  • Отправка outbound в новый geo с новой regulatory спецификой.

Что нельзя обходить:

  • Без marketing.email consent — никаких marketing email, даже если subject подписался когда-то и не отозвался. Pulse проверяет действительность consent на момент send, не на момент подписки.
  • Без personalization — UI banner может показываться только в degenerate mode «общий для всех», без таргетинга.
  • Right-to-erasure отменяет все active outbound queue для subject'а в течение 24 часов.

17.14 Roadmap включения каналов

Порядок включения каналов по milestone'ам вынесен в GitLab — см. issue #45. Технически каждый канал — независимая запись в Action contract + адаптер + (при необходимости) consent purpose.

17.15 Marketing patterns library

Назначение

Каталог формализованных маркетинговых приёмов (patterns), используемых при написании rules в Policy DSL и при создании outbound templates. Каждый pattern — переиспользуемый шаблон с человекочитаемым описанием, машинно-проверяемыми constraints и привязкой к архетипу аудитории.

Источник: методика Викентьева И.Л. («Приёмы рекламы и Public Relations»), адаптированная под software-стартап и privacy-VPN продукт + накопленный опыт product/marketing team.

Полный каталог приёмов вынесен в companion-документ marketing-patterns.md. Эта секция фиксирует только техническую интеграцию.

Архитектура

┌──────────────────────────────────────────────────────────────┐
│  pulse_curated.marketing_patterns (Directus collection)     │
│  - PV0001 Эффект края                                        │
│  - PV0002 Эффект Миллера                                     │
│  - PV0010 Кредит доверия                                     │
│  - ... 18+ patterns                                          │
└──────────┬───────────────────────────────────────────────────┘
           │ referenced by
   ┌───────┼────────────────────┐
   ▼       ▼                    ▼
┌──────────────────┐  ┌──────────────────────┐
│ Policy DSL rule  │  │ Template (banner/    │
│ rule.patterns:[] │  │ email/push/etc.)     │
│ rule.template_   │  │ template.patterns:[] │
│ variants[]       │  │                      │
└────────┬─────────┘  └──────────┬───────────┘
         │ activate constraints  │
         ▼                       ▼
┌─────────────────────────────────────────────────────────────┐
│  Brand guardrails verifier (§8.5.4 класс B)                 │
│  Применяет composition constraints из patterns к шаблонам  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  Audit log: pattern usage в decision_context                │
└─────────────────────────────────────────────────────────────┘

Жизненный цикл pattern'а

  1. Добавление. Маркетолог (или Pulse Chat draft-rule-suggester в M4) предлагает новый pattern. В Directus admin создаётся запись в pulse_curated.marketing_patterns. Поля: id, name, category, description, applies_to (action types), constraints (machine-checkable), archetype_affinity, contraindications, sources, version.
  2. Review. Pattern проходит approval queue: legal review (особенно для constraints, влияющих на legal compliance — fearmongering_score, paranoia_language), marketing review (effectiveness и actuality для текущего рынка).
  3. Publishing. Утверждённый pattern становится доступен в DSL: rule.patterns: [PV0001] (пример с реальным id из каталога).
  4. Effectiveness tracking. Audit log записывает использование pattern'а в каждом decision. Meta-analytics (M4) считает effectiveness per pattern × archetype × action.
  5. Versioning. Изменения определения pattern'а — новая версия (PV0001.v3). Старые rules/templates продолжают ссылаться на старую версию до явного апдейта.
  6. Deprecation. Pattern с низкой effectiveness или признанный устаревшим попадает в auto-deprecation queue (meta-automation, см. issue #45). Финальный archive — через human approval.

Связь с Policy DSL

Rule в Policy DSL может ссылаться на patterns через поле patterns: list<pattern_id> (см. §8.3.2). При наличии:

  • Compile-time check: SMT-валидатор + brand-guardrails verifier применяют constraints всех ссылаемых patterns к template шаблона. Несовместимые patterns (например, PV0003 Non finito + transactional action) — compile error.
  • Audit transparency: decision-record содержит patterns_applied: [PV0001, PV0002, PV0011], что даёт human-readable объяснение «почему сообщение выглядит именно так».
  • Discovery в Rule Studio: при создании rule UI предлагает релевантные patterns на основе target action type и текущего surrounding context.

Связь с Archetype

Archetype dimension (§9.2.6) — soft preference signal, используется только для ranking (не как hard condition):

  • Pattern имеет поле archetype_affinity: list<archetype> — рекомендация, для каких архетипов pattern наиболее эффективен.
  • Rule может иметь template_variants с per-variant archetype_affinity — для выбора variant'а внутри одного action.
  • Обязательный fallback archetype_affinity: any гарантирует, что null-archetype subject не остаётся без обслуживания.

Что archetype делает: при выборе между альтернативами (несколько применимых rules / несколько template variants одного rule) Pulse предпочитает вариант с лучшим archetype-affinity.

Что archetype НЕ делает: не исключает subject'а из применимости правила. Любой subject (включая archetype: null) получает action; меняется только tone/copy/visual, не сам факт получения.

Privacy considerations

Использование patterns с composition constraints не требует дополнительного consent — это техническая проверка качества контента, не персонализация.

Использование archetype_affinity для ранжирования — требует consent.personalization, потому что зависит от archetype subject'а.

Никаких patterns с явно манипулятивным intent в catalogue не включается. Соответствие проверяется review-процессом + brand-guardrails (§8.5.4): fearmongering_score, paranoia_language, false_urgency и подобные constraints автоматически отлавливают попытки добавить манипулятивные patterns.

Эволюция каталога

Catalogue живой: маркетинг-команда расширяет его по мере наблюдения, удаляет недействующие patterns. Каждое изменение проходит через Directus admin с audit log, сложные изменения — через approval queue. Версионирование каждого pattern'а отдельное.

Источник вдохновения (Викентьев) — стартовая база; со временем catalogue будет содержать patterns, специфичные для privacy-software-стартапа, которых нет в исходной методике.


18. Testing strategy

18.1 Назначение

Pulse — формальная система с контрактами, SMT-валидацией, audit log, privacy invariants. Стандартная test-пирамида необходима, но недостаточна. Architectural-уровневые требования к тестированию (coverage targets, обязательные классы тестов, privacy invariant assertions) — часть спецификации. Operational guide (как именно тестировать, какие инструменты, какой CI pipeline, какой test corpus) вынесен в companion-документ testing.md.

18.2 Test pyramid

Распределение нестандартное из-за формальной природы Pulse: больше property-based и snapshot, чем в обычном backend.

   E2E in Directus UI                  ~5%
   Federation (multi-node)             ~5%
   Integration (pulse-core + Postgres) ~15%
   Property-based + snapshot           ~20%
   Unit                                ~55%

Детали — testing.md §2.

18.3 Architectural coverage targets

Coverage targets — часть архитектурных требований, не рекомендация. Нарушение → блокер мерджа в pulse/main.

СлойCoverageMutation score
Identity-mapping service≥95%≥80% nightly
Consent token validation≥95%≥80% nightly
SMT validator≥90%≥80% nightly
Audit log + Merkle≥90%≥80% nightly
DSL compiler + runtime≥85%
Contract compiler (codegen)≥85%
Ingest pipeline≥80%
Action dispatch≥80%
ML training≥70%
LLM Gateway≥75%
Directus extensions (UI)≥60%
Federation / DHT integration≥70%

Полная таблица с обоснованием — testing.md §9.

18.4 Обязательные классы тестов

Каждый класс ниже — architectural requirement для Pulse. Реализация описана в companion-документе.

КлассЧтоРеализация
Privacy invariant assertionsВсе 17 invariants из §3.1, §3.2, §3.3, §3.4, §3.5 + §9.2.6.4 имеют explicit assertiontesting.md §4.1
Contract evolution testsCanonicalization, compatibility classification, multi-version live read, migration scriptstesting.md §5.1
SMT validation regression corpusGolden corpus conflict/dead/rate-limit/budget casestesting.md §5.2
Snapshot tests на codegenRust types / TS types / SQL migrations / tool catalog — insta-snapshotstesting.md §5.3
DSL property-basedParser idempotence, type-checker soundness, privacy invariant preservationtesting.md §5.4
Decision replay testsBit-exact replay по decision_id; drift detectiontesting.md §5.5
Federation multi-node testsContract negotiation, signed pointers, Merkle, Eclipse/Sybiltesting.md §6
Meta-automation safety testsAuto-rollback, champion-challenger, emergency halt, bounded blast radiustesting.md §7
Compliance testsGDPR rights (access/erasure/rectification/portability), EU AI Act boundaries, retentiontesting.md §8
Chaos tests (nightly)Network partition, latency, packet loss, identity-mapping failovertesting.md §6.3

18.5 Test corpus в репозитории

Golden test data — первоклассный артефакт репозитория kontinuum-pulse:

kontinuum-pulse/tests/
  ├── corpus/
  │   ├── contracts/     # тестовые версии контрактов
  │   ├── rules/         # valid / conflict / dead / privacy-violation
  │   ├── decisions/     # golden replay-corpus
  │   └── events/        # event sequences
  ├── snapshots/         # insta snapshots всех codegen artifacts
  └── proptest-regressions/  # найденные proptest counterexamples

Anonymized snapshot реальных audit-log decisions периодически (раз в месяц) добавляется в decisions/replay-corpus.jsonl — это даёт real-world distribution тесты, не synthetic. Детали — testing.md §12.

18.6 CI/CD pipeline (4 уровня)

УровеньКогдаДлительность
PR pipelineКаждый PR<10 минут
Pre-mergeПеред merge в main15-30 минут
NightlyРаз в сутки1-3 часа
Pre-releaseПеред production deployдо 6 часов

Полное содержание каждого — testing.md §10.

18.7 Связь с roadmap

Эволюция testing-инфраструктуры по milestone'ам — см. testing.md §13 и milestone-роадмап в GitLab (issue #45).

18.8 Принципы (architectural commitments)

  1. Invariants как тесты, не документация. Каждый privacy invariant имеет CI-проверяемый assertion.
  2. Property-based для formal слоёв. Contract / DSL / SMT — property-based first.
  3. Snapshot для codegen. Любые изменения в генераторе видны в diff PR.
  4. Audit log как test oracle. Historical decisions — regression corpus.
  5. Privacy assertions in production. Runtime checks превращают privacy-нарушение в incident.
  6. Тесты Pulse не зависят от real LLM. Mock-провайдер; real LLM — отдельный optional smoke.
  7. Federation тесты deterministic. turmoil в CI; реальная сеть — только nightly/pre-release.

Статус и roadmap

ВерсияДатаИзменения
v0.12026-05-18Initial draft. Полная архитектура, контракты, deployment, federation, threat model. Открытые вопросы Q1-Q10.
v0.22026-05-19Зафиксирована single-UI стратегия: добавлен §4.4-bis (Deployment topology через Directus admin), переписан §10.4 (shared Postgres + separate identity-mapping), переписан §13.4 (Billing integration через shared DB), полностью переработан §15 (M0-M5 Roadmap с UI-эволюцией), добавлен §15-bis (UI evolution strategy). Backend-архитектура без изменений.
v0.32026-05-19PostHog-inspired дополнения в roadmap. M1: feature flags как degenerate-rule + group dimension в Semantic Layer. M2: survey support (Action + events + collections) + cohort-sync action для reverse-ETL. M3: chronological event-tape API + session timeline tab в Audit Explorer + insight extensions (Funnel/Lifecycle/Retention/Stickiness) поверх Directus Insights. Контракты, инварианты, federation, threat model — без изменений.
v0.42026-05-19Введён meta-уровень автоматизации. Добавлен §15-ter «Meta-automation strategy» с концепцией «Pulse applied to itself», тремя классами (safety/self-management/discovery), принципом reversible-only auto-act, emergency halt в DHT. Дополнения в roadmap: M2 — auto-handling GDPR + auto-audit preflight + self-healing action targets; M3 — pulse_internal.* events, meta-actions, rule_stats, drift detection, anomaly detection, champion-challenger, auto-deprecation, auto-rollback, self-meta health; M4 — cohort discovery, pattern mining → rule drafts, auto-feature suggestion, auto-detection privacy incidents, Discovery view + Approval Queue UI; M5+ — budget allocation optimization, LLM smart routing, privacy budget tracking, adaptive auto-tuning. Изменения аддитивные: контракты, инварианты, federation, threat model без структурных правок (только §12 получает новую угрозу — компрометация meta-уровня).
v0.52026-05-19Добавлен §17 «Marketing channels integration» — полноценный inventory маркетинговых каналов и их связи с Pulse. Классификация (in-product / direct outbound / content inbound / community-led / PR / paid ads). Подробное описание каналов: UI banners, onboarding, inbox, surveys, local notifications, transactional/marketing email, push, marketing site (Plausible, не Pulse), first-touch UTM attribution, referral, ambassador, Discord/Matrix opt-in linking, PR launch markers, paid ads (UTM-only). Provider adapter architecture для внешних провайдеров. Attribution model (first-touch, in-product, referral chain, self-reported, multi-touch как derived feature). Полный channel × action × telemetry inventory с milestone-tags. Consent matrix per channel. Roadmap включения каналов синхронизирован с §15. Изменения аддитивные.
v0.62026-05-19Полноценная интеграция методики Викентьева. Создан companion-документ marketing-patterns.md — каталог из 18 patterns по 7 категориям (composition, trust building, pattern breaking, spiral, social proof, stereotype handling, privacy-domain). Глоссарий §2 расширен: marketing pattern, composition constraint, stereotype, archetype. §8.3.2 Policy DSL — добавлены поля rule.patterns и rule.archetype_targeting с privacy compile-time checks. §8.5.4 Brand guardrails — введён класс B (composition constraints) со связью к pattern library. §9.2.6 — новый dimension subject.archetype в Semantic Layer с derivation logic, privacy invariants 7-9, lineage. §12.2 — новые угрозы T16 (psychographic targeting) и T17 (manipulative composition). §15 M2 — initial release marketing_patterns collection; M3 — composition constraints в verifier + archetype dimension (self-reported source); M4-M5+ — pattern suggestion, effectiveness analytics, ML-based archetype derivation. §16 — новые open questions Q11-Q13. Новая §17.15 — техническая интеграция Marketing patterns library (архитектура, lifecycle, связь с DSL/archetype/privacy).
v0.72026-05-19Зафиксированы три мета-концепции архитектуры, влияющие на любое будущее решение: (1) Новый §3.6 «Architectural intent: privacy-by-design vs privacy-as-checkbox» — формальный design-принцип, что защиты Pulse являются architectural invariants, не compliance overlay; контраст подходов с примером T16; правило «защита должна быть оправдана минимум двумя из трёх уровней — legal/product-fit/strategic». (2) Новый §11.8 «Per-region regulatory overlay matrix» — таблица регионов (EU/CA/BR/UK/CN/RU/AU) с overlay-требованиями, явный список «что одинаково для всех регионов» и «что меняется как overlay»; процесс добавления нового региона без изменения архитектуры. (3) Appendix A в marketing-patterns.md — Behavioural substitution guide для маркетолога: как использовать behavioural conditions вместо запрещённого archetype-targeting в billing-actions. Изменения чисто документационные; backend-архитектура, контракты, federation без изменений.
v0.82026-05-19Archetype упрощён до soft preference signal: устранена overengineered psychographic-инфраструктура, признанная избыточной для useful applications. §9.2.6 переписан с self-reported-only derivation (удалены behavioural + ML-cluster sources); §8.3.2 заменил archetype_targeting (hard condition) на template_variants с per-variant archetype_affinity (soft ranking); §2 глоссарий уточнён; §12.2 удалена угроза T16 (psychographic targeting) — вырождается до minor risk при soft-only использовании; бывшая T17 (manipulative composition) стала T16; §15 M5+ удалена строка про ML-based archetype derivation, M3 уточнена; §16 Q11 закрыт как settled-by-design; §17.15 «Связь с Archetype» переписана под soft-preference; §3.6 пример обновлён под текущую формулировку invariant. marketing-patterns.md синхронизирован (v0.3): §4 архетипы и §5 использование переписаны под soft-preference; Appendix A переформулирован под compile-time запрет hard archetype-conditions. Обоснование (§9.2.6.5): self-reported survey даёт прозрачность, легальную простоту, дешёвую реализацию, Goodhart-устойчивость.
v0.92026-05-19Consistency pass — устранены 9 inconsistencies, накопленных за v0.1–v0.8 итерации. (1) §17.15 диаграмма: устаревшее rule.archetype_targeting заменено на rule.template_variants[]. (2) §9.2.6.2: устранена ссылка на несуществующий action-параметр flow_variant_by_archetype; onboarding personalization идёт через стандартный template_variants механизм. (3) Унифицированы имена composition constraints first_block_carries_key_idea / last_block_carries_key_idea в §8.5.4, M3 roadmap, Q12, marketing-patterns.md §1 и §3.1. (4) §9.1.2 явно помечен как base subset; добавлена таблица с extended purposes (marketing.push, marketing.email_engagement_tracking, marketing.attribution, local_notifications, external_identity_linking) со ссылкой на §13.4 и §17.13. (5) Pattern effectiveness analytics унифицирован на M4 (Q13 + §17.15 lifecycle). (6) §8.3.1.1 — новый раздел «Namespaces в DSL»: формально описаны subject.*, subject.features.*@v, ml.*@v, consent.* с правилами разрешения, версионированием и compile-time ограничениями (archetype запрещён в applies_when). (7) §15 M0 добавлен endpoint /api/templates/validate и явный milestone для brand-guardrails verifier класс A (legal/brand); класс B остаётся в M3. (8) marketing-patterns.md §1: phantom PV0042 в структурном примере заменён на реальный PV0001. (9) marketing-patterns.md §5 ссылка §3.1 invariant 7-9 исправлена на §9.2.6.4 (расширения §3.1). Архитектурных изменений нет, только consistency.
v0.102026-05-19Зафиксирована testing strategy. Добавлен §18 «Testing strategy» в спеку: test pyramid, architectural coverage targets (с mutation-score для critical files), 10 обязательных классов тестов (privacy invariants, contract evolution, SMT corpus, snapshot codegen, DSL property-based, decision replay, federation multi-node, meta-automation safety, compliance, chaos), test corpus в репозитории, CI/CD 4-уровневый pipeline, синхронизация с roadmap M0-M5+, 7 architectural commitments. Создан companion-документ testing.md с operational guide (~700 строк): тесты по слоям Pulse, cross-cutting, Pulse-specific approaches, federation, meta-automation, compliance, tools matrix, layout test-corpus в репозитории. Изменения чисто аддитивные; backend-архитектура, контракты, federation, threat model без изменений.
v0.112026-05-19Pre-implementation freeze package. Добавлена директория contracts/ с machine-readable артефактами, которые блокировали старт M0: (1) telemetry-contract-schema.yaml — JSON Schema Draft 2020-12 для Telemetry-контрактов (категории, event-типы, primitive-types); (2) action-contract-schema.yaml — JSON Schema для Action-контрактов (targets, parameters, side_effects, audit classes, rate-limits); (3) consent-purposes.yaml — полный M0-реестр из 11 purposes по 4 группам (ops/meta, analytics, personalization/experiments, marketing/channels) + regional overlays для EU/UK/CA/BR; (4) openapi.yaml — OpenAPI 3.1 для M0 endpoints pulse-core (/events/ingest, /dsl/validate, /templates/validate, /actions/dispatch, /gdpr/{access,erasure,portability}, /health/*, /version); (5) ddl/01_identity_mapping.sql — DDL для отдельного identity-mapping Postgres instance (salt-generations, subjects, devices, marketing_subjects, access_audit, allowed_callers); (6) ddl/02_pulse_curated.sql — Directus-managed схема в shared Postgres (consent_purposes, consent_grants, rules, templates, flags, marketing_patterns, experiments, surveys, cohort_destinations, outbound_providers, approval_queue, segments, onboarding_flows); (7) ddl/03_pulse_runtime.sql — pulse-core-managed схема (events с monthly-partitioning, events_rejected, feature_store, decisions, action_acks, outbound_events, audit_log с Merkle-chain + genesis row, rule_stats, experiment_assignments, federation_pointers, gdpr_jobs). Новые секции спеки: §9.1.6 «Identity-mapping service API» (полный список операций, авторизация Ed25519, изоляция БД, инварианты, rate-limit); §10.6 «Bootstrap procedures» (pre-bootstrap ceremony, DB-bootstrap, contracts-bootstrap, service bring-up, smoke verification, salt-rotation procedure); §10.7 «Performance budgets для M0» (SLO для ingest, identity-mapping, decision engine, GDPR, audit log, resource budgets, verification). Изменения чисто аддитивные.
v0.14.82026-05-28Implementation milestone (post-spec): decision spine + §7.4 unified dispatch landed in kontinuum-pulse. Документационных правок самой спецификации нет — все обещания этих секций сохраняются буквально; меняется только статус «реализовано». Реализовано в коде (см. implementation-status.md для полной карты vs §15 M0): (1) Звено event→featuresfeature_derive.rs пишет в pulse_runtime.feature_store (§8.1.3) realtime-колонки set-on-event; оконные метрики читаются query-time из pulse_runtime.events (§9.2.7 «timestamps вместо derived-from-now»). (2) Звено hydrationsubject_context.rs собирает SubjectContext из feature_store + query-time оконных counts + load_consent (consent keyed by subject_id в M0 — конфляция marketing-pseudonym, см. §9.1.6 note). (3) Звено orchestrationdecision_engine.rs с in-memory compiled rule cache (на старте + /rules/reload deploy hook), applies_when + forbidden_when (§8.3.3 «drop rules where forbidden_when triggers»), priority desc / rule_id asc tie-break, оба evaluation-пути (event-triggered и cohort-scan, §8.3.3), experiment-gate с assign + variant_required (§8.4), archetype template-variant ranking (§9.2.6). (4) Звено provenancepulse_runtime.decisions пишется с реальным rule_id/rule_uuid/subject_id/contract_hash/triggering_event_id; идемпотентность через детерминированный decision_id = blake3(rendered idempotency_key) (§7.9.5). Фейк-провенанс M0.STUB / subject_id=[0u8;32] устранён. (5) §7.4 unified dispatch — единый dispatch_decision tail для всех side_effect классов: pre-flight (consent/rate-limit/brand, §7.4 «check consent / check rate limit») → INSERT decision (real provenance) → INSERT ack по классу (§7.9.3: none/ui_statesuccess; outbound/billingdeferred) → audit append (§8.3.3 «audit log → action dispatched»). Outbound/billing на движковом пути оседают deferred parked (next_retry_at = NULL, reason recipient_unresolved) — фактический wire-send ждёт identity/CRM-слой (§13.4); retry-worker SELECT обновлён, чтобы пропускать parked. (6) /actions/dispatch low-down — синтез фейк-row устранён: при существующей engine-decision это «appends-ack» режим (§7.4 «engine creates the row, dispatch appends ack») с UPSERT ack, без перезаписи провенанса; при отсутствии — manual-путь с обязательным реальным subject_id и rule_id='EXTERNAL.MANUAL' (честный маркер). §16 Q-резолюции / архитектура / контракты / DDL / валидаторы — без изменений. Известные открытые границы (вне pulse-core): identity-mapping resolve_marketing + recipient resolution из CRM (§13.4), баг make_interval(hours => double precision) в воркерах auto_rollback/auto_tune.
v0.14.72026-05-20§16 open questions audit. Каждый из Q1–Q13 получил status tag и явное resolution. Сводная таблица (§16.0) показывает: 5 CLOSED — Q1 (per-identity subject в DDL), Q7 (Z3 SMT в dsl-grammar.md §9), Q8 (latency budgets в §10.7.3), Q9 (отдельный experiments.assignment_salt в DDL), Q11 (archetype settled by design в v0.8); 7 DEFERRED к конкретному milestone — Q2/Q3/Q5/Q13 → M4, Q4/Q12 → M3, Q6 → M2 (с зафиксированной direction для каждого); 1 OPEN — Q10 (audit log hash-references как personal data) требует pre-prod lawyer review с EU/UK GDPR counsel, но НЕ блокирует M0 implementation. Каждый Q-block получил «Resolution» подсекцию с (a) ссылкой на solving spec section / DDL element или (b) явным milestone owner-ом отложенного решения. Никаких изменений в архитектуре, контрактах, DDL, валидаторах. M0 implementation теперь не имеет архитектурно-открытых блокеров.
v0.14.62026-05-20Bootstrap manifests (реализация spec §10.6 для local development). Создана kontinuum-pulse/bootstrap/: (1) docker-compose.yaml с 5 сервисами — postgres-identity-mapping (port 15433, isolated per §9.1.1), postgres-shared (port 15432, host'ит billing + pulse_curated + pulse_runtime + directus_system), directus 11.5 (port 8055), identity-mapping + pulse-core (Rust services, ports 18081/18080); все на internal bridge network, named volumes для clean wipe. (2) Multi-stage Dockerfile с cargo-chef caching, debian:bookworm-slim runtime, non-root user pulse (UID 10001), strip'нутый binary, build-arg CRATE выбирает целевой бинарь. (3) .env.example с DEV-defaults для всех secrets (DB passwords, Directus KEY/SECRET, admin email) + явный disclaimer что production значения приходят из offline ceremony §10.6.1. (4) Bash scripts: apply-ddl.sh (psql на оба instance из contracts/ddl/0{1,2,3}_*.sql), seed.sh (initial salt generation 1 + audit_log_genesis + consent_purposes + DEV placeholder federation_root_keys), smoke.sh (6 verification checks по §10.6.5: /health/{live,ready}, /version JSON shape, active_generation row, consent_purposes count > 0, audit genesis). (5) Python helper gen-seed-consent-purposes.py — читает contracts/consent-purposes.yaml и эмитит idempotent INSERT-ы (ON CONFLICT DO UPDATE); протестирован — корректно генерирует 12 purposes. (6) Makefile в kontinuum-pulse/: make bootstrap-full = up → apply → seed → smoke за одну команду; отдельные targets для инкрементальной работы; cargo check/clippy/test + validate.py обёрнуты. (7) bootstrap/README.md — usage, layout, troubleshooting, что НЕ входит (production secrets, mTLS, K8s manifests — отложены на M1/M2). Никаких изменений в spec architecture, контрактах, DDL, валидаторах.
v0.14.52026-05-20DSL grammar v0.1 + kontinuum-pulse scaffold. (1) Создан dsl-grammar.md v0.1 — формальная грамматика Pulse Policy DSL: lexical structure, type system, operator precedence, namespaces (subject.*, subject.features.*@v, ml.*@v, consent.granted(), now()), built-in functions (coalesce, days_since, length, contains), error model с 9 кодами (E001–E009) + 3 warning'ами (W001–W003), SMT compilation outline (Z3 transpilation tables), privacy invariants (compile-time запрет hard archetype в applies_when). (2) Создан contracts/test-corpus/dsl/ с 6 valid примерами (cohort definitions, rule applies_when, NULL handling, IN clauses, numeric arithmetic, consent gating) и 7 invalid примерами (по одному на error code) с expected error metadata. (3) Создан рабочий Rust workspace kontinuum-pulse/ (на уровне корня репозитория, не submodule пока): Cargo workspace с 3 крейтами (pulse-core, identity-mapping, pulse-client), /health/{live,ready} + /version endpoints на Axum, contracts/ как symlink на single source of truth, cargo check --workspace + cargo clippy -- -D warnings зелёные, оба binary smoke-tested live (curl /version returns valid JSON). Никаких изменений в существующих контрактах.
v0.14.42026-05-20pulse-llm-gateway переключён с TypeScript/Node на Rust в §10.4. Причина: единственный backend service, нарушавший user-memory правило «core-крейты Rust». Преимущества Rust: (1) консистентный backend stack — один Docker image type, общий CI lane, единые tracing/logging; (2) type-safe PII strip через PiiTagged<T> newtypes (privacy invariant из §9 enforced compile-time); (3) единый codegen tool catalog из action-contract-schema.yaml через build.rs — нет drift между Rust и TS codegens; (4) shared types с pulse-core через tonic/serde для internal RPC. Prompt iteration не блокируется: prompts в pulse_curated.templates (data-driven через Directus admin), не в коде gateway. Implementation: Axum + async-openai + anthropic-rs. Архитектурные impact'ы: декларация в §10.4 обновлена, остальные секции (§13.7 SDK layout, §10.2 deployment table) уже были консистентны. Никаких изменений в контрактах, DDL, валидаторах.
v0.14.32026-05-20Second-pass consistency review — закрыты 13 issues после deep cross-file review. Critical (3): (C-1) Добавлен §13.1.1 «App-side action endpoint» — формат payload {decision_id, action_type, parameters, signature}, expiration semantics, idempotency на App-side (24h cache decision_ids), WebSocket vs long-poll выбор. Закрывает chicken-and-egg app.banner_shown.decision_id. (C-2) §7.9.5 расширен NULL-timezone fallback rule: при subject.timezone = NULL используется UTC day + tz_fallback: true маркер в decisions.action_parameters. (C-3) §6.9.2 scope rule: sample_strategy: random допустим только для events с purpose IN (ops, meta); для analytics/personalization/marketing — только sticky. Important (7): (I-1) Добавлена metric app_sessions_last_7d — закрывает orphan column feature_store.sessions_last_7d. (I-2) Добавлена dimension subject.churned_at (populated_by app.tier_changed when new_tier=free and reason IN [cancel,refund]) — закрывает orphan column. (I-3) §9.3.3 actor_kind resolution rule: subject-triggered → subject; admin → admin; meta/system-triggered (log_only, gdpr-worker, retention) → system; federation peer → federation_peer. log_only всегда system независимо от триггера. (I-4) subject.first_paid_at description расширен явной refund-semantics: paid_at immutable, не сбрасывается при refund; subject.has_paid = true означает «когда-либо платил», для «активного платящего» — subject.tier != 'free'. (I-5) Metric paired_devices_countpaired_devices_total с явным «monotonic» комментарием и path для active count через identity-mapping.devices. (I-6) §10.6.6 предупреждение о sticky sampling reshuffle при salt rotation — рекомендация generation-aware aggregation, audit_log entry sampling.reshuffled. (I-7) Compliance fix: новый consent purpose billing_transactional (legal_basis: contract, GDPR Art. 6(1)(b)). send_transactional_email.requires_consent изменён с ops на billing_transactional — receipt emails больше не subject to ops opt-out. (I-8) marketing.push в consent registry помечен available_from_milestone: M1 — purpose declared для EU regional compatibility, но не используется в M0 actions. (I-9) contracts/index.md: corrected dimensions count 7 → 12 (после добавления subject.churned_at). (I-10) m0-user-journey.md: bumped v0.1 → v0.2, sync version spec v0.14 → v0.14.3. Minor validator gaps (3): (V-1) validate.py: добавлен check_dimension_populated_by_events — каждый populated_by_events entry в semantic-layer dimensions должен ссылаться на существующий event_type. (V-2) Добавлен check_event_version_orderingremoved_in_version (если present) должно быть > added_in_version. (V-3) Добавлен check_rate_limit_per_template_per — enforce enum [subject_id, global]. Оба валидатора зелёные.
v0.14.22026-05-20Post-v0.14.1 consistency pass — закрыты 11 issues найденных code-reviewer. (1) GdprAccessBundle.feature_store: убрано устаревшее days_since_install, добавлены installed_at/onboarding_completed_at/paid_at/churned_at. (2) veil_bytes_total убрано из GDPR bundle (Veil → v0.2.0). (3) §6.9.2: убрана пометка "M2+" с sample_strategy: random — фактически используется в M0 (app.error_occurred, pulse_internal.rate_limit_hit). (4) §7.8 requires_subject_fields — зафиксирована two-level семантика: action-level ceiling vs template-level narrower subset, brand-guardrails class A enforce'ит template-level ⊆ action-level. (5) В app.startup добавлены optional timezone (IANA) и locale (BCP-47) fields — populate dimensions subject.timezone и subject.preferences.locale. (6) synced_with_spec унифицирован на v0.14.2 во всех 10 contract файлах. (7) GDPR bundle feature_store расширен полным набором lifecycle timestamps. (8) contracts/index.md bumped v0.5 → v0.6, spec sync → v0.14.2. (9) DDL feature_store.veil_* колонки помечены комментарием "reserved for v0.2.0". (10) marketing-patterns.md Appendix A: days_since_install < 14 заменено на subject.installed_at > now() - 14d. (11) Создана директория test-corpus/canonical/ с 4 минимальными fixture sets (telemetry/action/semantic-layer/consent-purposes) — input YAML + expected canonical.json + blake3 placeholder для cross-language parity test. Оба валидатора зелёные.
v0.14.12026-05-19Lifecycle dimensions refactor: metric days_since_install (derived-from-now, refresh=batch) заменён на immutable timestamp dimension subject.installed_at. Причина: derived-from-now() метрики stale на refresh interval, не replay-able (один и тот же decision input при replay даёт другой output), и замораживают гранулярность bucket'а. Тот же паттерн применён ко всем moment-данным: добавлены dimensions subject.installed_at, subject.first_paid_at, subject.last_seen_at, subject.onboarding_completed_at (все TIMESTAMPTZ, populated set-on-null или per-event). DSL пишет subject.installed_at < now() - 7d вместо days_since_install >= 7 — bucket derive'ится at-query, не materialized. Зафиксирован новый design-инвариант §9.2.7 «Timestamps вместо derived-from-now counters» с правилом и допустимыми исключениями (sliding-window counts для analytics ОК, для tight-deadline rules — запрещены). Изменения: cohorts installed_but_unpaid/onboarding_dropped переписаны, DDL feature_store колонка days_since_install INTEGERinstalled_at TIMESTAMPTZ, telemetry app.install_completed потерял ссылку в derived_features (тeперь populates dimension, не metric), оба валидатора зелёные.
v0.14.02026-05-19M0-готовность: закрыты все 14 outstanding gap'ов из послереспонсного review v0.13.1. Критические (1-5): (1) Добавлены App pairing events (pairing_initiated/completed/failed) + metric paired_devices_count. (2) Добавлены identity flow events (identity_created/imported/import_failed) для recovery analytics. (3) Добавлены App error/crash events (error_occurred, crashed, network_state_changed) с sampling per event. (4) Новый §7.8 «Template engine» — зафиксирован Handlebars + sandbox config + locale strategy + requires_subject_fields whitelist для PII protection. (5) Новый §6.9 «Sampling» — sample_rate field в EventType meta-schema, sticky vs random strategy, derivation compensation rule (enforced by validate.py). Серьёзные (6-10): (6) Добавлены pulse_internal.contract_loaded, gdpr_job_completed, federation_pointer_received, rate_limit_hit events для observability. (7) Новый §7.9 «Action error handling и retry policy» — глобальные конвенции для retry/timeout/dead-letter; retry_policy schema добавлена в action-contract-schema (meta-schema bumped 1.0 → 1.1). (8) §7.9.5 — subject.timezone dimension добавлена в semantic-layer; {send_day} placeholder теперь tz-aware; решён ambiguity для outbound_message idempotency. (9) Test corpus расширен: 6 новых invalid examples (expired_token, malformed_token_signature, purpose_mismatch, schema_hash_mismatch, jti_replay, enum_violation). (10) Новый §10.9 «Canonical serialization для content hashing» — YAML→JSON→JCS→blake3 pipeline, deterministic rules, reference test vectors. Cleanup (11-14): (11) validate.py расширен: rate_limit window pattern, idempotency placeholder validation, retry_policy enum check, requires_subject_fields ↔ semantic-layer dimensions parity, sample_rate compensation enforcement, corpus purpose parity. (12) Новый validate_enums.py — DDL ↔ openapi ↔ action-contract enum parity checker (29 CHECK constraints извлекаются и сверяются с YAML enum'ами). (13) Новый contracts/event-types-overview.md — read-only человекочитаемая таблица всех 35 event-types с purpose/retention/sample. (14) Новый docs/pulse/m0-user-journey.md — end-to-end App user journey (install → onboarding → pairing → banner → payment → unsubscribe → GDPR) с явными references на events/actions/metrics/tables в каждом шаге. Также: telemetry contract разрос с 22 до 35 event-types; actions добавили retry_policy/dispatch_timeout_ms/requires_subject_fields. validate.py + validate_enums.py — оба зелёные. Архитектурных изменений нет, только аддитивные расширения.
v0.13.12026-05-19Contracts v0.1.0 hardening (App scope). После саморевизии v0.13 контрактов выявлены coverage gaps и cross-contract inconsistencies; pass без архитектурных изменений. (1) telemetry/v0.1.0.yaml переписан с App-фокусом: убрана veil.* категория (vendored в v0.2.0+), добавлены event-types для критических App funnel: app.onboarding_step_completed, app.onboarding_finished, app.banner_shown, app.banner_dismissed, app.banner_clicked, app.email_{delivered,opened,clicked,bounced,unsubscribed}, app.consent_changed, app.permission_changed, app.feature_used, pulse_internal.client_event_dropped — итого 22 events. (2) actions/v0.1.0.yaml: send_email расщеплён на send_transactional_email (requires ops, bypass marketing consent) и send_marketing_email (requires marketing.email), оба получили subject/from_address/reply_to параметры; send_push и show_in_app_inbox_message явно отложены на v0.2.0. (3) semantic-layer/v0.1.0.yaml переписан App-focused: убраны veil_* метрики, добавлены onboarding_*, banner_*, email_*_rate; placeholder cohort recently_churned удалён; все metrics получили explicit refresh policy (realtime / continuous_aggregate с refresh_interval / batch с cron schedule). (4) Создан contracts/test-corpus/ с 17 valid event payloads (по одному per event_type), 4 invalid payloads (schema mismatch / consent missing / missing required / unknown event_type), и 4 valid action dispatch payloads. (5) Создан contracts/validate.py — cross-contract validator: проверяет purpose existence, source_event references, derived_features ↔ metrics parity, test-corpus coverage per event_type/action_type, enum_values violations в corpus. Запускается в pre-merge CI. (6) При первом прогоне validator нашёл 22 ошибки в v0.13 контрактах (unused purpose, mismatched email_* event names) — все исправлены до подтверждения v0.13.1. Архитектурных изменений нет.
v0.132026-05-19M0-блокирующие артефакты. Закрыты критические gap'ы, найденные post-v0.12 анализом «что не хватает для полноценной реализации». (1) Созданы реальные M0-контракты v0.1.0 в contracts/telemetry/v0.1.0.yaml (11 event-types: veil.{session_started, session_ended, relay_error}, app.{startup, install_completed, tier_changed}, billing.{payment_received, refund_issued}, pulse_internal.{rule_evaluated, action_dispatched, decision_suppressed}), contracts/actions/v0.1.0.yaml (3 actions: show_ui_banner, send_email, log_only — с rate_limit'ами и полными parameters), contracts/semantic-layer/v0.1.0.yaml (4 dimensions + 10 metrics + 3 cohorts + lineage invariants). (2) Новый §9.1.4-bis «Consent token wire format» — нормативная спецификация: CBOR + COSE_Sign1 + Ed25519, claim set с jti для replay protection, TTL ≤ 1ч, clock skew ±60s, client_key location per-platform (Keychain/Keystore/DPAPI/Stronghold), key_id = BLAKE3 публичного ключа. (3) Новый §13.7 «Client SDK contract» — публичный API крейта pulse-client (Rust canonical + TS/Kotlin обёртки), buffer/retry семантика (LMDB/sled persistence, exponential backoff с jitter, Retry-After honoring, backpressure при 429), transport (HTTP/1.1+, mTLS для server-side callers, zstd compression, 256 KiB batch cap), schema validation опциональна, ConsentTokenProvider trait, privacy invariants (нет crash-reporter exfiltration, нет PII в general logger). (4) Новый §10.8 «Data retention policies» — per-table retention (events следует Telemetry contract, audit_log forever, decisions 2y, gdpr_jobs 5y), partition rotation worker, erasure-job vs retention interaction (built-in 24h SLA), backup retention + encryption keys split, compliance metadata в audit_log. (5) openapi.yaml GdprAccessBundle schema расширена с полной структурой: contract_versions_used, feature_store snapshot, events_summary (aggregated counts), decisions, outbound_messages, consent_history, audit_records с entry_hash, merkle_proof envelope. Архитектурных изменений нет; всё аддитивно. После v0.13 разработчик имеет всё необходимое, чтобы начать M0 implementation.
v0.122026-05-19Consistency pass для v0.11 freeze package — устранены 15 inconsistencies, выявленных code-reviewer'ом сразу после freeze. Высокий приоритет: (1) 01_identity_mapping.sql seed allowed_callers расширен до полного набора операций §9.1.6 (resolve_marketing, list_generations, rotate_salt); добавлен caller pulse-admin-cli. (2) openapi.yaml: ActionDispatchRequest.target дополнен значением pulse (meta-actions из §15-ter теперь dispatch'аются легально). (3) openapi.yaml: ActionAck.status дополнен failed (совпало с DDL action_acks.status enum). (4) openapi.yaml: IngestResponse.results.status дополнен unknown_event_type и malformed_token (совпало с DDL events_rejected.reason enum). (5) consent-purposes.yaml: явный legal_basis: consent для personalization и experiments (убрана зависимость от неявного default). (6) M0 roadmap §15: убрана пометка «без Merkle пока» — Merkle-chain hashing локально с M0 (DDL уже содержит prev_hash/entry_hash/audit_log_genesis); DHT-публикация Merkle-корней остаётся в M3. (7) consent-purposes.yaml EU regional: default_state_override: opt_in исправлен на opt_out (GDPR Art. 7(2) — consent не может быть pre-checked); добавлен inline comment. (8) §10.6.2 переписан: указан полный путь seed-файлов (часть встроена в DDL, часть generated by bootstrap pipeline), добавлен «Scope note» о том, что в repo. (9) openapi.yaml: ErrorResponse перенесён в components/schemas (был на корневом уровне — невалидный документ, все $ref сломаны). Средний приоритет: (10) §17.13: marketing.email_engagement исправлен на marketing.email_engagement_tracking (совпало с consent-purposes.yaml). (11) §10.4: добавлено описание pulse-admin-cli (operator-side CLI-утилита для критических операций). (12) testing.md bumped v0.1 → v0.2; добавлены тесты freeze-package в M0 roadmap (schema validation, DDL ↔ OpenAPI enum parity, allowed_callers parity), bootstrap smoke test, performance budget gating. (13) §12.2 T15: audit class auto_decision исправлен на существующий compliance_review_required (соответствует AuditClass enum в action-contract-schema.yaml). (14) 02_pulse_curated.sql: добавлена таблица federation_root_keys (упоминалась в §10.6.1, не существовала в DDL). (15) contracts/README.md: добавлен раздел «Что НЕ входит → M1+ таблицы БД» с явным указанием, что DDL — только M0 baseline (например, launch_events из §17.7 — M3, добавляется отдельным файлом). Архитектурных изменений нет.