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/.
Оглавление
- Назначение
- Глоссарий
- Принципы и инварианты
- Архитектурный обзор
- Контрактный слой
- Telemetry contract
- Action contract
- Pipeline: 5 слоёв
- Cross-cutting горизонтали
- Места выполнения (deployment)
- Federation и согласование версий
- Модель угроз
- Интеграционные точки
- Версионирование и миграции
- Roadmap
- Открытые вопросы
- Marketing channels integration
- 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 может рекомендовать действия (например, эскалацию), но не управляет тикетами. |
| Аналитическим DWH | Pulse держит минимально необходимый 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 класс, идемпотентность. |
| Contract | Telemetry 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 Log | Append-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. |
| PII | Personally 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 (Ст+/Ст−) | Устойчивый паттерн восприятия определённой аудиторией. Положительный (Ст+) — активируется приёмом; отрицательный (Ст−) — корректируется альтернативным фреймом. Терминология из методики Викентьева. |
| Archetype | Soft 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
- No plaintext E2E content: Pulse никогда не видит расшифрованного содержимого vault'ов, файлов, сообщений. Только метаданные событий (типы, счётчики, агрегаты).
- Consent-first ingest: событие, не покрытое валидным consent'ом subject'а, не попадает во внутренние слои Pulse. Либо отбрасывается на клиенте, либо отбрасывается на boundary, в любом случае не сохраняется.
- Pseudonymous by default: ни один внутренний слой Pulse, кроме identity-mapping service, не видит реальных идентификаторов Kontinuum. Только
subject_id. - Right to erasure: при отзыве consent или удалении аккаунта все события и derived features соответствующего subject'а удаляются из feature store. Audit log сохраняет только псевдоним и хеш-метаданные.
- No raw data to external LLM: текст, отправляемый LLM-провайдеру, не содержит идентификаторов, сырых событий и любых полей, помеченных как PII в Telemetry contract.
3.2 Decisioning invariants
- Three-engine separation: каждое решение классифицируется по доминирующему движку (rule / ML / LLM). Гибридные решения декомпозируются на цепочку с явным указанием вклада каждого. Не допускается «чёрный ящик из всех трёх».
- Symbolic constraints dominate ML: rule engine задаёт пространство допустимых действий, ML оптимизирует внутри этого пространства. ML не имеет права предлагать действие, запрещённое правилами.
- LLM not in critical path: LLM может генерировать варианты, объяснения, запросы — но решения, влияющие на биллинг, capacity, compliance, проходят через symbolic verifier до исполнения.
- Determinism of replay: для любого decision_id из audit log при наличии артефактов (контракт по hash, версия модели, версия правил) можно воспроизвести входы и выходы решения.
3.3 Contract invariants
- Contract is single source of truth: типы событий и действий нигде не описаны кроме как в Telemetry/Action contract. Все runtime'ы (Rust ingest, TS Directus integration, SQL feature store, LLM tool catalog) либо генерируются из контракта, либо валидируются против него.
- Content-addressed versions: версия контракта = его blake3-хеш. Строковый идентификатор
v0.4— только для коммуникации между людьми. - Signed updates only: переход на новую версию контракта в production-канале возможен только через signed pointer с порогом подписей (см. §11).
3.4 Audit invariants
- Every decision logged: ни одно действие Pulse не покидает подсистему без записи в audit log.
- Append-only: audit log не поддерживает UPDATE/DELETE. Коррекции делаются compensating-записями.
- Merkle-chained: записи audit log образуют Merkle-цепочку. Корни периодически публикуются в DHT (§11.4), обеспечивая внешне-проверяемую неизменность истории.
3.5 Operational invariants
- Pulse failure does not block products: отказ Pulse-core (ingest недоступен, ML не отвечает, LLM gateway упал) не должен блокировать продуктовый user flow. Деградация: события буферизуются на клиенте, действия идут по дефолтным правилам.
- 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 | продукт → Pulse | Telemetry contract | events типизированы, consent-проверены до ingest, pseudonymous |
| Action out | Pulse → продукт | Action contract | идемпотентные, валидированные, с decision_id для traceability |
| Human / LLM I/O | маркетолог ↔ Pulse | NL 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 на правилах и шаблонах без причины дороже. |
Решение даёт три ключевых свойства:
- Минимум инфраструктуры на старте — Directus уже есть, шаблоны permission/auth/audit готовы.
- Privacy invariant сохраняется — identity-mapping в отдельной БД, Directus admin физически не имеет к ней доступа (см. §9.1).
- 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/main | Production-канал. Используется central и большинством федерации. |
pulse/beta | Канал предрелизных версий. Используется частью узлов для проверки совместимости. |
pulse/staging | Канал для внутренних тестов команды. |
pulse/<x> | Кастомные каналы для federation-сегментов (например, EU-only consent-purposes). |
Каждый канал в любой момент времени имеет signed pointer:
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.rsRust-крейтов Pulse — для compile-time типов. - В Nx-таргете
pulse-contracts:gen— для генерации TS/SQL артефактов в монорепо. - В CI при publishing новой версии — для валидации и генерации distribution-bundle.
5.7 Runtime modes
Pulse поддерживает три режима использования контракта в рантайме:
- Compile-time only (default для central core): бинарь Pulse-core собран против фиксированного contract_id. Изменение контракта = пересборка + редеплой. Преимущество: zero runtime overhead.
- Runtime-load: бинарь загружает контракт на старте по
contract_idиз конфигурации или DHT-канала. Используется для клиентов и edge-нод, чтобы они могли получать новые правила без обновления приложения. - Hot-reload (опционально, milestone M3): бинарь следит за изменением pointer'а канала и атомарно переключается на новую версию контракта. В пределах switching window поддерживается dual-mode (in-flight операции на старой, новые — на новой). Доступно только в central, не в клиентах.
6. Telemetry contract
6.1 Назначение
Telemetry contract описывает исчерпывающий набор типов событий, которые могут попасть в Pulse. Событие, тип которого не описан в активном контракте, отбрасывается на boundary — даже если эмиттер был обновлён раньше.
6.2 Структура
Контракт — корневой документ со списком категорий событий. Каждая категория содержит набор event-типов.
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_id | subject | Псевдонимизированный идентификатор. Всегда первое поле. |
event_type | string | {category}.{name}, например veil.session_started. |
schema_hash | bytes32 | Hash контракта, под которым emit-сторона валидирует событие. |
emitted_at | timestamp | UTC время эмита на источнике. |
ingested_at | timestamp | Заполняется ingest-слоем Pulse. Не доверяется источнику. |
source | enum | client / edge / central / billing — где было сгенерировано. |
consent_token | bytes | Подписанный 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.
6.5 Consent binding
Каждое событие в контракте обязано декларировать purpose — одну из целей в Consent purposes registry. Ingest-слой Pulse при приёме события:
- Извлекает
subject_idиpurpose. - Запрашивает Consent Store: разрешён ли этот purpose для этого subject'а на момент
emitted_at. - Если нет — событие отбрасывается до записи в 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-е событие.
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 для агрегатов:
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 Структура
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_personalization7.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_key | Template, по которому 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 | Гарантии | Аудит |
|---|---|---|
none | Read-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):
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:
| Корневой ключ | Тип | Источник |
|---|---|---|
params | object | Action parameter template_params (map<string,string> из action contract) |
subject | object | Whitelist subject dimensions, заявленных в template's requires_subject_fields (см. ниже) |
i18n | object | Локализованные строки из templates.content.i18n[{locale}] |
now | timestamp | Дата рендера (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:
- Парсер Handlebars проверяет syntax валидность.
- Brand-guardrails class A (см. §8.5.4) валидирует обязательные элементы (unsubscribe link для marketing email, sender address, etc.).
- Все использованные переменные сравниваются с
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):
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_human7.9.3 Success criteria per side_effect
| side_effect | success | failure |
|---|---|---|
none | row создан в decisions + audit_log | DB error (retry by orchestrator, не action) |
ui_state | app ack'нул (POST /actions/ack) ≤ 60 s | timeout, app ack reject (e.g. banner_id collision) |
outbound_message | provider 2xx И delivery webhook ≤ 24 h | provider 4xx (не 429); delivery webhook = bounced |
billing_state_change | billing service ack + idempotency check passed | billing reject; idempotency conflict |
external_provider_call | provider 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 fromdecided_at.{send_day}— для outbound_message actions: если subject имеет dimensionsubject.timezone, используется local day; иначе UTC day.
Это гарантирует что юзер в Tokyo не получит promotional email "в 16:00" просто потому что UTC day boundary падает на середину их дня.
Fallback при subject.timezone = NULL (subject ни разу не эмитил app.startup с timezone полем):
{send_day}resolved как UTC day (date_trunc('day', decided_at AT TIME ZONE 'UTC')).- В соответствующую
pulse_runtime.decisionsrow пишетсяaction_parameters.tz_fallback: true(informational; не часть action contract). - Маркетолог при создании rule может явно установить
parameters.send_afterс фиксированным UTC offset, чтобы избежать неподходящих локальных часов для anonymous-timezone subjects. 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 view | Materialized view с прокаткой агрегатов окнами (session_count_7d, bytes_avg_30d). Обновляется по приходу событий. |
Каждая фича в Online/Offline store ссылается на:
subject_idfeature_name(из Metrics Registry)metric_version(версия определения в Semantic Layer)value(типизированное)computed_atsource_events_hash(опционально, для reproducibility)
8.1.4 On-device aggregation (опциональный режим)
Для subject'ов без consent на analytics.usage, но с consent на personalization, клиент:
- Хранит сырые события локально, не отправляя их в central.
- По расписанию вычисляет локальные фичи (на тех же определениях из Semantic Layer).
- Отправляет в 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_7d | P(subject inactive on day 7 from now) | LightGBM |
churn_prob_30d | P(subject churned in 30 days) | LightGBM |
intensity_score | Z-score текущей активности vs baseline subject'а | Robust z-score (median) |
product_affinity | Per-product propensity score (Veil/Tether/Send/...) | Multi-output GBM |
uplift_per_action | Δ(reward) от применения action vs not | Causal forest / X-learner |
next_best_action | argmax(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.*vsml.*: 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 Структура правила
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 Node | Federation: 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_whenunsat при любых 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 Структура эксперимента
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: N | PV0002 Эффект Миллера | Количество отличных «идей-ударений» ≤ N (default 5) |
first_block_carries_key_idea | PV0001 Эффект края | Первый блок шаблона несёт ключевую идею |
last_block_carries_key_idea | PV0001 Эффект края | Последний блок усиливает или резюмирует ключевую идею |
expectation_payoff | PV0020 Оттяжка | Если headline ставит вопрос — payoff присутствует в последнем блоке |
series_credibility | PV0010 Кредит доверия | В серии ≥4 marketing-сообщений ≥1 содержит acknowledged limitation |
direct_denial_forbidden | PV0051 Коррекция Ст− | Шаблон не содержит прямого отрицания негативного стереотипа категории |
abstract_grounded | PV0052 Ярлык-образ | Абстрактные термины (privacy/security/encryption) при первом упоминании сопровождаются physical analogy |
social_proof_anonymized | PV0040 Чужая победа | Никаких конкретных идентифицируемых subject'ов без opt-in |
threat_factuality | PV0041 Общий враг | Утверждения об угрозах документированы со ссылкой |
fearmongering_score | PV0041 Общий враг | Мера эмоционального давления ниже порога |
paranoia_language | PV0071 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 Identity & Consent Layer
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 остаётся хеш-метаданные).
9.1.2 Consent purposes registry
Декларативный список целей сбора и обработки данных. Часть контракт-канала (отдельный YAML-документ, ссылается из Telemetry contract).
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.
9.1.3 Consent Store
Хранит для каждого subject'а:
- Список granted purposes с timestamp grant + источник (UI / Pairing / Admin).
- Expiration (если purpose имеет ограничения по сроку).
- История изменений (append-only).
Реализация: таблица в существующем billing/ Postgres. Pulse-core читает через узкий API, который при возможности кэширует на короткое окно (минуты).
9.1.4 Consent token
Каждое событие, эмитированное клиентом, сопровождается consent_token:
consent_token := sign(client_key, {
subject_id,
purpose,
granted_at,
token_issued_at,
expires_at # короткий, минуты-часы
})Ingest при приёме события:
- Проверяет подпись токена клиентским ключом.
- Сверяет
purposeс purpose'ом event-типа из Telemetry contract. - Сверяет
granted_atс Consent Store (защита от replay устаревшим токеном).
Это даёт non-repudiable доказательство наличия consent в момент эмита, независимо от того, что произошло потом.
9.1.4-bis Consent token wire format
Точная спецификация формата токена. Это нормативная часть; 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):
| Key | Name | Value |
|---|---|---|
| 1 | alg | -8 (EdDSA) |
| 4 | kid | bstr — client_key_id; 32 bytes BLAKE3 публичного ключа клиента |
| 33 | purpose | tstr — purpose ID из consent-purposes.yaml |
Claims (CBOR map с string keys в payload):
| Claim | Type | Required | Описание |
|---|---|---|---|
sub | bstr (32) | yes | subject_id — то же значение, которое identity-mapping вернёт ingest'у |
gen | u16 | yes | generation salt'а identity-mapping |
pur | tstr | yes | purpose ID (дубль из protected headers для удобства парсинга) |
gat | u64 | yes | granted_at — unix epoch seconds, момент grant'а в Consent Store |
iat | u64 | yes | token_issued_at — момент создания токена |
exp | u64 | yes | expires_at — unix epoch seconds |
jti | bstr (16) | yes | unique token id (BLAKE3-derived from `iat |
nbf | u64 | no | not_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 / macOS | Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) | Generated при первом запуске; ротация при app.tier_changed или раз в 90 дней |
| Android | Android Keystore (KeyProperties.PURPOSE_SIGN, StrongBox если есть) | То же |
| Windows | DPAPI / CredentialManager (per-user) | То же |
| Linux | Stronghold (см. docs/app/services/stronghold.md) | То же |
| Headless / CLI | Stronghold с user-supplied password | manual 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:
- Identity-mapping service помечает
(identity, subject_id)mapping как удалённый. - Pulse-core запускает batch удаления всех записей с этим
subject_idиз Feature Store и Online Store. - Audit Log не модифицируется (append-only invariant); записи с этим subject_id остаются, но subject_id уже не resolvable — становится «висящим псевдонимом».
- 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+); ниже — операционная семантика.
Операции:
| Operation | Caller (allowed) | Args | Returns | Side effects |
|---|---|---|---|---|
pseudonymize | pulse-core-ingest | identity_id, event_ts | subject_id, marketing_subject_id?, generation | Создаёт mapping, если не существует; пишет в access_audit |
resolve_subject | pulse-core-decisioning, pulse-core-gdpr | subject_id, generation, reason | identity_id (только для GDPR) или OK-маркер | access_audit row |
resolve_marketing | pulse-core-decisioning | subject_id или marketing_subject_id | парный псевдоним | access_audit row |
erase_subject | pulse-core-gdpr | subject_id, reason | OK | Устанавливает erased_at; mapping становится нерезолвимым; access_audit |
rotate_salt | pulse-admin-cli (root) | — | новый generation | Создаёт новый salt_generations row; обновляет active_generation |
list_generations | любой allowed caller | — | список generations | read-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; mappingsubject_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-документ, привязанный к контракт-каналу.
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.archetype — soft preference signal для ranking patterns, template variants и onboarding flows. Это не condition-dimension: ни одно правило не использует archetype в applies_when.
9.2.6.1 Определение
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 используется
- Pattern affinity ranking (§17.15). При выборе между несколькими применимыми правилами Pulse предпочитает rule, привязанные patterns которого имеют лучший
archetype_affinityк subject'у. - Template variant selection (§8.3.2). Внутри одного rule с
template_variantsвыбирается вариант с подходящимarchetype_affinity. Обязательный fallbackarchetype_affinity: any. - Onboarding flow variant. Onboarding-action
start_onboarding_flow(§17.3.2) поддерживает personalization через стандартный механизмtemplate_variantsсarchetype_affinity(§8.3.2). Никакого специального параметра в action contract не требуется — onboarding обрабатывается как любой другой action с template-вариантами. - 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)
- Archetype требует
consent.personalization. При отсутствии —archetype: null. В этом случае все rules работают в generic-режиме (выбирается variantarchetype_affinity: any). - Subject видит и редактирует свой archetype через App → Settings → Privacy. Это часть GDPR right-to-access + right-to-rectification.
- 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-инвариант с тремя причинами:
Stale data. Любой derived-from-now() аггрегат имеет refresh-задержку (batch / continuous_aggregate). Decision engine, читающий «days_since_install = 5», может работать с реальным значением 6 — rule, ожидающий «≥ 7», не сработает до следующего batch run'а. На границах суток это лотерея.
Не replay-able. Decisions воспроизводятся через
pulse_runtime.decisionsrow + восстановление input feature snapshot. Если фича derive'ится отnow(), при replay через неделю значение другое — то же решение даст другой output. Это ломает audit reproducibility (см. §9.3) и regression-тесты (см. testing.md §5.5).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_whenrules с 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 Структура записи
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_personalization | Pulse devs, marketing, sec-audit |
outbound_communication | Pulse devs, marketing, compliance, sec-audit |
billing_credit | Pulse devs, finance, compliance, sec-audit |
tier_change | Pulse devs, finance, compliance, sec-audit, human-approval queue |
external_provider_call | Pulse devs, sec-audit |
meta | Pulse 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 можно:
- Поднять
contract_id, скачать артефакт из DHT. - Восстановить версии моделей, правил, метрик из лога.
- Запустить decision-engine в replay mode с теми же входами.
- Сверить, что результат идентичен записанному.
Это используется:
- Для дебага «почему этому 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 device | App, Veil, Tether, Send. Источник сырых данных. | Полное: все данные доступны в plaintext. |
| Owner Node (Tier 0/1 self / org) | Узлы, принадлежащие subject'у или org'у. | Доверенные псевдонимизированные данные. |
| Community Node (Tier 2) | Узлы третьих лиц. | Не доверены: только инвариантные операции. |
| Central (Pulse Core) | Кластер сервисов команды платформы. | Доверен в плане pseudonymized data. |
| External LLM | Anthropic / OpenAI / другие. | Не доверен; PII-strip обязателен. |
10.2 Распределение слоёв по уровням
| Слой / компонент | Client | Owner Node | Community Node | Central | External 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 assignment | ✓ | ─ | ─ | config | ─ |
| Experiment statistics | ─ | ─ | ─ | ✓ | ─ |
| Identity-mapping | ─ | ─ | ─ | ✓ sole | ─ |
| Consent Store source | ─ | ─ | ─ | ✓ | ─ |
| Consent Store replica | ✓ | ─ | ─ | ─ | ─ |
| Semantic Layer compute | ✓ types | ✓ types | ✓ types | ✓ compute | catalog |
| 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)
Эти артефакты должны существовать ДО первого запуска сервисов:
- Root signing key для federation channel pointers — Ed25519, генерируется в offline ceremony (ops-team yubikey + Shamir share); публичный ключ зафиксирован в
pulse_curated.federation_root_keysчерез миграцию. - 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. - Initial
global_salt— 32 байта от CSPRNG, фиксируется вidentity_mapping.salt_generations(generation=1, salt=…). Никогда не логируется в plain text. - Directus admin — учётная запись с MFA, отдельная роль
pulse-adminсоздана через миграцию Directus с правами наpulse_curated.*, read-only наpulse_runtime.*. - TLS-сертификаты для внутренних gRPC между сервисами (mTLS, отдельный CA для internal mesh).
10.6.2 Database bootstrap
Порядок применения миграций (через dbmate/sqlx migrate, контролируемо CI):
identity_mappingPostgres (отдельный instance):01_identity_mapping.sql— схема, таблицы, indices. Также содержит inlineINSERTдля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).
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_logrows цепляются к этому корню.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 должны быть опубликованы:
- Telemetry contract v0.1.0 — содержит минимальный набор event-типов M0 (
veil.*,app.*,billing.*,pulse_internal.*); content-hash вычисляется при загрузке, публикуется в DHT поcontract-artifact:{hash}. - Action contract v0.1.0 —
show_ui_banner,send_email,log_onlyдля M0. - Semantic layer v0.1.0 — определения базовых метрик (
dau,mau,tier,veil_bytes_total). - 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):
- POST
/health/readyкаждого сервиса → 200. - Test event через
/events/ingestс тестовым consent-token → 207 сaccepted=1. - Создание тестового rule в Directus → POST
/dsl/validate→ 200valid=true. - Чтение
audit_log— проверка непрерывности hash-chain (последнийprev_hashсовпадает с предыдущимentry_hashили genesis).
Любой провал → rollback развёртывания.
10.6.6 Salt rotation procedure
Salt rotation — отдельный bootstrap-like процесс:
- Ops оператор вызывает
rotate_salt(требует 2 операторов через approval queue + MFA). - Identity-mapping создаёт новую
salt_generationsrow (generation+1). - Обновляется
active_generation. - Backfill job: пересчёт
subject_idдля активных subjects в новом generation; старые generation остаются read-only для исторических данных. - Feature store backfill: новые
subject_idдублируют существующие записи (на время transition). - После полного backfill — старые generation помечаются
deprecated_at; удаление по retention. - Каждый шаг — отдельная 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.events | per-event retention_days (см. Telemetry contract) | Минимально необходимый период. После — drop partition. По умолчанию: analytics=90d, ops=365d, billing-related=7y. |
pulse_runtime.events_rejected | 30d | Debug/drift detection; не нужен на дольше |
pulse_runtime.feature_store | пока subject активен | Перезаписывается, не растёт. При erasure-job — row deleted. |
pulse_runtime.decisions | 730d (2 года) | Replay, attribution, dispute resolution |
pulse_runtime.action_acks | 730d | Связан с decisions |
pulse_runtime.outbound_events | 365d | Provider correlation, complaint investigation |
pulse_runtime.audit_log | forever | Compliance, Merkle chain integrity (см. §9.3, §10.6) |
pulse_runtime.rule_stats | 365d | Rolling baseline для dead-rule detector |
pulse_runtime.experiment_assignments | до 90d после ends_at соответствующего experiment | Sticky assignment нужен только пока эксперимент жив + grace period |
pulse_runtime.federation_pointers | 90d | Debug; реальный pointer state — в DHT |
pulse_runtime.gdpr_jobs | 1825d (5 лет) | Регуляторное требование на хранение compliance evidence |
identity_mapping.access_audit | 1825d | Compliance + 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-й день каждого месяца —
CREATE TABLE events_YYYY_MM PARTITION OF events FOR VALUES FROM ... TO .... Идемпотентно черезCREATE TABLE IF NOT EXISTS+ проверкуpg_partitions. - 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). - Vacuum / analyze. Стандартный Postgres autovacuum; явный manual
VACUUM ANALYZEпосле drop partition.
10.8.3 Erasure interaction
Erasure-job (§9.1.5) удаляет данные раньше retention deadline:
- Identity-mapping помечает subject как erased.
pulse-retention-workerв специальном «erasure mode» проходит по таблицам с user-data (events, decisions, outbound_events, feature_store) и удаляет rows с этимsubject_id.- Audit-log НЕ модифицируется — там остаётся
subject_idкак «висящий псевдоним», но без mapping обратимости (см. §9.1.5). - Партиции
eventsНЕ дропаются при erasure (partition содержит данные многих subjects); удаление — построчно (DELETE с индексом поsubject_id).
SLA: полная erasure данных subject'а ≤ 24 ч (см. §10.7.4); compliance-окно регуляторов — 30 дней с момента запроса.
10.8.4 Backup retention
| Артефакт | Backup frequency | Backup retention | Encryption |
|---|---|---|---|
identity_mapping БД | hourly + daily | 90d | Отдельный ключ (ceremony output) |
pulse_runtime.* + pulse_curated.* | hourly + daily | 30d | Стандартный кластерный ключ |
pulse_runtime.audit_log (S3 cold) | daily | forever | Per-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 purposes | pulse_curated.consent_purposes (региональный набор) | Новая запись в коллекции |
| UI-элементы legal disclosure | Templates в pulse_curated.templates с regional variants | Новые templates, regional-routed |
| Mandatory fields в marketing email | Brand-guardrails class A constraints (региональные) | Новые regional constraints, активируются по subject.country |
| Data storage location | Federation channel selection (pulse/eu, pulse/cn, ...) | Новый federation channel со специфической топологией |
| Right-to-X endpoints | Existing GDPR endpoints (access/erasure/portability) | Региональная alias-маршрутизация на те же endpoints |
| Sensitive purpose categories | Marking в Telemetry contract + Semantic Layer | Региональная аннотация на existing fields |
Добавление нового региона
Шаги:
- Legal review: какие специфические требования (consent purposes, mandatory disclosures, data residency, retention).
- Consent purposes: добавить новые записи в
pulse_curated.consent_purposesс regional scope. - Templates: создать regional variants для marketing/transactional с обязательными disclosure-элементами.
- Brand-guardrails: добавить regional class A constraints (sender address, opt-out language, regional claims restrictions).
- Federation channel (если data residency требует): новый канал
pulse/{region}, отдельный publishing pipeline, отдельные Tier 1 nodes в регионе. - 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 store | Identity-mapping изолирован в отдельном сервисе/БД с отдельными ключами шифрования; PII-поля шифруются at rest. |
| T2 | Подмена контракта без авторизации | Content-addressing + multi-sig pointer + monotonic seq + previous_hash chain. |
| T3 | Rollback-атака на старую уязвимую версию контракта | previous_hash chain + monotonic seq + explicit rollback: true flag with reason + audit-logging. |
| T4 | Freeze-атака (узел держат на старом pointer'е) | Pointer имеет expires; узлы периодически re-fetch'ат; alerting при stale pointer'ах. |
| T5 | LLM exfiltration через prompt injection | PII-strip on input; tool catalog не содержит write-actions; audit логирует все обращения. |
| T6 | Подмена ack'а от action target → audit log lies | Ack подписан target'ом; audit хранит signature; mismatch обнаруживается reconciliation-jobом. |
| T7 | Telemetry-flood от скомпрометированного клиента | Rate-limiting per (client_pubkey, event_type); edge Node может early-drop; ingest идемпотентен по event_id. |
| T8 | Утечка через timing / count side-channel в действиях LLM | Cost-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 позволяет восстановить activity | Merkle root содержит только hash'и; полные записи доступны только central; root reveals только factum-existence. |
| T11 | Compromise одного signing-key для контрактов | Multi-sig threshold (2-of-3+); key rotation; emergency revocation через Tier 0. |
| T12 | DSL injection через NL-rule-drafting (LLM выдала malicious правило) | LLM никогда не публикует напрямую; SMT-валидатор проверяет правило; human approval перед publish. |
| T13 | Re-identification через cross-reference Feature Store | K-anonymity-проверка при экспорте агрегатов; DP-noise для cohort'ов <K subjects. |
| T14 | DoS на ingest | Rate-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. |
| T16 | Manipulative 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):
{
"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'ается локально с emissionpulse_internal.client_event_dropped(drop_reason=action_expired).
Response (App → pulse-core, async через /actions/ack endpoint pulse-core):
{
"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 через стандартный
storagecapability. - 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 |
|---|---|---|---|
billing | Directus | Customers, subscriptions, payments, plans, capacity | full (как сейчас) |
pulse_curated | Directus | Rules, templates, experiments, consent purposes, approval queue, segments | full через стандартные коллекции |
pulse_runtime | Pulse-core | Events, features, decisions, audit | read-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-veil | pulse-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):
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_sizeevents accumulated, (2)flush_intervalelapsed, (3) explicitflush()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-Afterhonoring (max 5 min).400/401(consent expired / malformed) → НЕ retry; drop event + emitpulse_internal.client_event_dropped(если квоты позволяют).2xx/207→ ack; для 207 — обработать per-event status в response (results[]).
- Backpressure. При непрерывных 429 на ≥5 минут клиент переходит в degraded mode: уменьшает
emitrate в 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 решает финально).
13.7.6 Consent token integration
Клиент НЕ хранит сам 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 contract | contract_id (blake3) + semver tag |
| Action contract | contract_id (blake3) + semver tag |
| Semantic Layer | contract_id (blake3) + semver tag |
| Consent purposes registry | contract_id (blake3) + semver tag |
| Rules Registry | per-rule R0042.v3; bundle has own contract_id |
| ML models | model_name@version; артефакты в отдельном storage (S3 / Kontinuum DHT) |
| LLM prompts | prompt_id@version; артефакты иммутабельны |
| Audit Log | epoch идентификатор партии |
14.2 Совместимость
При смене активного pulse/main контракта Pulse-core поддерживает multi-version live read:
- Принимает события с
schema_hashстарой или новой версии (пока обе находятся вeffectivewindow). - Парсит по соответствующим compiled types.
- В Feature Store записывает в общем формате (через миграцию полей по карте rename'ов).
При changes уровня breaking Pulse-core останавливает приём, пока operator явно не пройдёт через migration script. Это намеренная защита от тихих несоответствий.
14.3 Миграции схем
Для каждого breaking-изменения публикуется отдельный artifact:
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: falseMigration 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 retentionAudit 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-product | Banners, modals, onboarding, inbox, surveys внутри App | Сильная: actions + telemetry прямо | Идеальная |
| Direct outbound | Email, push, in-app messages с consent | Сильная: actions через провайдеров | Хорошая, при строгом consent |
| Content inbound | Marketing site, blog, tutorials, SEO | Слабая: только first-touch | Идеальная |
| Community-led | Discord, Matrix, GitHub, ambassador, referral | Средняя: opt-in linking + referral | Идеальная |
| PR / launches | ProductHunt, Show HN, awesome-lists | Слабая: retrospective атрибуция | Хорошая для запусков |
| Paid ads | Google 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:
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_personalizationTelemetry:
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:
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: personalizationTelemetry:
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:
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: personalizationTelemetry:
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:
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 # отдельный purposeTelemetry: 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:
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_communicationTelemetry (через 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:
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.
Поток:
- Маркетинговая ссылка содержит UTM-параметры:
https://kontinuum.app/download?utm_source=hn&utm_medium=post&utm_campaign=launch_v2. - Сайт сохраняет UTM в localStorage (cookie-free) перед download'ом.
- 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: ...
}- Pulse записывает это как первое событие subject'а; UTM становятся dimension'ами в Semantic Layer:
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:
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:
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 для ambassadorTelemetry: 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:
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'а:
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:
- 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-reported | Onboarding 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
| Channel | Action | Key telemetry | Milestone |
|---|---|---|---|
| UI Banner | show_ui_banner | app.banner.shown/clicked/dismissed | M0 |
| Onboarding flow | start_onboarding_flow | app.onboarding.* | M1 |
| In-app inbox | post_to_inbox | app.inbox.delivered/read/clicked/dismissed | M2 |
| Survey | show_survey | survey.shown/responded/dismissed | M2 |
| Local notification | schedule_local_notification | app.local_notification.* | M2 |
| Transactional email | send_transactional_email | email.delivered/bounced/complained | M1 |
| Marketing email | send_marketing_email | email.opened/clicked (opt-in) | M2 |
| Push notification | send_push | app.push.received/opened | M2 |
| First-touch attribution | (passive) app.installed | UTM dimensions в Feature Store | M1 |
| Referral | generate_referral_code, grant_referral_reward | app.referral.* | M3 |
| Discord/Matrix linking | link_external_identity | community.discord.* (opt-in) | M4 |
| Ambassador | invite_to_ambassador, grant_ambassador_reward | app.ambassador.* | M4 |
| PR launch marker | mark_launch_event (meta) | n/a | M3 |
| Paid ads attribution | (passive) расширение app.installed | UTM/server-side conversion reporting | M5+ |
Конкретные семвер-версии 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 purpose | Notes |
|---|---|---|
| UI banner | personalization | Можно работать с уменьшенной частотой без consent |
| Onboarding | personalization | Базовый onboarding допустим без, расширенный — с |
| Inbox | personalization | Critical operational alerts — ops |
| Survey | personalization | Anonymous surveys — ops |
| Local notification | local_notifications | Отдельный purpose |
| Transactional email | ops | Обязательный для product operation |
| Marketing email | marketing.email | Opt-in only, unsubscribe в каждом письме |
| Email open tracking | marketing.email_engagement_tracking | Отдельный sub-purpose |
| Push | marketing.push / ops | Visible vs silent |
| Discord/Matrix link | external_identity_linking | Новый purpose |
| Paid ads attribution | marketing.attribution | Server-side privacy-preserving conversion only |
Что НЕ автоматизируется (всегда human):
- Создание нового шаблона marketing-сообщения (legal/compliance review).
- Изменение consent purposes (юридический документ).
- Подключение нового external provider (security review).
- Отправка outbound в новый geo с новой regulatory спецификой.
Что нельзя обходить:
- Без
marketing.emailconsent — никаких 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'а
- Добавление. Маркетолог (или 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. - Review. Pattern проходит approval queue: legal review (особенно для constraints, влияющих на legal compliance — fearmongering_score, paranoia_language), marketing review (effectiveness и actuality для текущего рынка).
- Publishing. Утверждённый pattern становится доступен в DSL:
rule.patterns: [PV0001](пример с реальным id из каталога). - Effectiveness tracking. Audit log записывает использование pattern'а в каждом decision. Meta-analytics (M4) считает effectiveness per pattern × archetype × action.
- Versioning. Изменения определения pattern'а — новая версия (
PV0001.v3). Старые rules/templates продолжают ссылаться на старую версию до явного апдейта. - 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-variantarchetype_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.
| Слой | Coverage | Mutation 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 assertion | testing.md §4.1 |
| Contract evolution tests | Canonicalization, compatibility classification, multi-version live read, migration scripts | testing.md §5.1 |
| SMT validation regression corpus | Golden corpus conflict/dead/rate-limit/budget cases | testing.md §5.2 |
| Snapshot tests на codegen | Rust types / TS types / SQL migrations / tool catalog — insta-snapshots | testing.md §5.3 |
| DSL property-based | Parser idempotence, type-checker soundness, privacy invariant preservation | testing.md §5.4 |
| Decision replay tests | Bit-exact replay по decision_id; drift detection | testing.md §5.5 |
| Federation multi-node tests | Contract negotiation, signed pointers, Merkle, Eclipse/Sybil | testing.md §6 |
| Meta-automation safety tests | Auto-rollback, champion-challenger, emergency halt, bounded blast radius | testing.md §7 |
| Compliance tests | GDPR rights (access/erasure/rectification/portability), EU AI Act boundaries, retention | testing.md §8 |
| Chaos tests (nightly) | Network partition, latency, packet loss, identity-mapping failover | testing.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 counterexamplesAnonymized 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 в main | 15-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)
- Invariants как тесты, не документация. Каждый privacy invariant имеет CI-проверяемый assertion.
- Property-based для formal слоёв. Contract / DSL / SMT — property-based first.
- Snapshot для codegen. Любые изменения в генераторе видны в diff PR.
- Audit log как test oracle. Historical decisions — regression corpus.
- Privacy assertions in production. Runtime checks превращают privacy-нарушение в incident.
- Тесты Pulse не зависят от real LLM. Mock-провайдер; real LLM — отдельный optional smoke.
- Federation тесты deterministic.
turmoilв CI; реальная сеть — только nightly/pre-release.
Статус и roadmap
| Версия | Дата | Изменения |
|---|---|---|
| v0.1 | 2026-05-18 | Initial draft. Полная архитектура, контракты, deployment, federation, threat model. Открытые вопросы Q1-Q10. |
| v0.2 | 2026-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.3 | 2026-05-19 | PostHog-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.4 | 2026-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.5 | 2026-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.6 | 2026-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.7 | 2026-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.8 | 2026-05-19 | Archetype упрощён до 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.9 | 2026-05-19 | Consistency 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.10 | 2026-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.11 | 2026-05-19 | Pre-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.8 | 2026-05-28 | Implementation milestone (post-spec): decision spine + §7.4 unified dispatch landed in kontinuum-pulse. Документационных правок самой спецификации нет — все обещания этих секций сохраняются буквально; меняется только статус «реализовано». Реализовано в коде (см. implementation-status.md для полной карты vs §15 M0): (1) Звено event→features — feature_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) Звено hydration — subject_context.rs собирает SubjectContext из feature_store + query-time оконных counts + load_consent (consent keyed by subject_id в M0 — конфляция marketing-pseudonym, см. §9.1.6 note). (3) Звено orchestration — decision_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) Звено provenance — pulse_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_state → success; outbound/billing → deferred) → 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.7 | 2026-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.6 | 2026-05-20 | Bootstrap 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.5 | 2026-05-20 | DSL 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.4 | 2026-05-20 | pulse-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.3 | 2026-05-20 | Second-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_count → paired_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_ordering — removed_in_version (если present) должно быть > added_in_version. (V-3) Добавлен check_rate_limit_per_template_per — enforce enum [subject_id, global]. Оба валидатора зелёные. |
| v0.14.2 | 2026-05-20 | Post-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.1 | 2026-05-19 | Lifecycle 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 INTEGER → installed_at TIMESTAMPTZ, telemetry app.install_completed потерял ссылку в derived_features (тeперь populates dimension, не metric), оба валидатора зелёные. |
| v0.14.0 | 2026-05-19 | M0-готовность: закрыты все 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.1 | 2026-05-19 | Contracts 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.13 | 2026-05-19 | M0-блокирующие артефакты. Закрыты критические 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.12 | 2026-05-19 | Consistency 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, добавляется отдельным файлом). Архитектурных изменений нет. |