Skip to content

Pulse M0 — End-to-End User Journey

Версия: v0.2 · Синхронизация: spec v0.14.3 · Последнее обновление: 2026-05-20

Walk-through того, как M0-implementation Pulse работает на полном жизненном цикле App-пользователя: от установки до первой оплаты. Цель — дать новому разработчику и маркетинг-команде единую mental model, не заставляя их сшивать её из 3300 строк спецификации, 25 файлов в contracts/, и трёх sql-схем.

Каждый шаг подписан конкретными артефактами:

  • 📥 эмитируемые events (из contracts/telemetry/v0.1.0.yaml)
  • возможные действия Pulse (из contracts/actions/v0.1.0.yaml)
  • 📊 обновляемые feature_store / metrics (из contracts/semantic-layer/v0.1.0.yaml)
  • 🗃️ задеваемые таблицы (contracts/ddl/)

См. также: specification.md, contracts/event-types-overview.md, testing.md.


Acт 0 — App is downloaded, key generation begins

User action: Скачивает App, нажимает «Get Started».

App-side internals:

  1. Generates Ed25519 identity keypair via IdentityManager.
  2. Stores private key in platform keychain (Keychain / Keystore / DPAPI / Stronghold; см. spec §9.1.4-bis).
  3. Generates client signing keypair for Pulse consent tokens; registers client_key_id = BLAKE3(public_key) with identity-mapping service via pairing-like bootstrap.

Pulse-side:

📥 app.identity_created (purpose: ops, retention 5 лет)

  • identity_kind = primary
  • created_at

📥 app.install_completed (purpose: ops, retention 365 d)

  • app_version, platform, install_source

🗃️ identity_mapping.subjects — новая row с subject_id = blake3(identity_id || global_salt_v1). 🗃️ pulse_runtime.events — две row (install_completed, identity_created). 📊 feature_store.installed_at, first_seen_at set (immutable; DSL derives "days since" on-query via subject.installed_at < now() - Xd).

Возможное действие: log_only от welcome-rule (просто record что новый user; никаких баннеров пока).

Что НЕ происходит: marketing email не отправляется — у пользователя нет marketing.email consent (default opt-out по EU regional override, см. consent-purposes.yaml).


Акт 1 — Onboarding flow

User action: Проходит default onboarding flow (onboarding.default.v3): explain → create-space → connect-device-optional → notification-permission.

📥 app.onboarding_step_completed × 4 (per step)

  • flow_id = onboarding.default.v3
  • step_id = explain | create_space | connect_device | grant_notifications
  • step_index = 0..3
  • duration_on_step_ms

📥 app.permission_changed (если разрешает notifications)

  • permission_id = notifications, new_state = granted (or denied)

📥 app.onboarding_finished (terminal)

  • outcome = completed | skipped | abandoned
  • total_duration_ms

📊 feature_store.onboarding_completed_at set; subject.onboarding_completed dimension becomes true. 📊 metric onboarding_completion_ratio = 1.0.

Возможные действия:

  • Если outcome = completedshow_ui_banner (placement = top) с welcome message and "tour" CTA, gated by requires_consent: personalization.
  • Если outcome = abandoned → rule в cohort: onboarding_dropped запускает log_only для аналитики, без вторжения.

Что НЕ происходит: Pulse НЕ "догоняет" брошенный onboarding email-ом — marketing.email consent ещё не давал.


Акт 2 — Multi-device pairing (optional)

User action: Хочет подключить телефон (если desktop был первым). Нажимает «Pair device» → видит QR-code.

📥 app.pairing_initiated

  • pairing_id = ULID, role = initiator, method = qr_code

(на втором устройстве — second app.identity_created? Нет: pairing связывает существующий identity с новым device_id; см. §9.1.6 identity-mapping API)

🗃️ identity_mapping.devices — new row (device_id, subject_id, pairing_id).

📥 app.pairing_completed или app.pairing_failed в зависимости от исхода.

📊 metric paired_devices_count ++.

Действие при pairing_failed: rule pairing_recoveryshow_ui_banner (placement = modal) с troubleshooting (без email — это active session).


Акт 3 — Feature exploration

User action: Использует core features: создаёт spaces, шарит файлы, sync через node.

📥 app.feature_used (sample_rate 10% sticky)

  • feature_id = spaces.create | sharing.invite | sync.completed | …
  • duration_ms, outcome

📊 metric app_sessions_total++ при каждом startup; feature_used_count (с compensation × 10) обновляется в derivation.


Акт 4 — Banner promotion (free → paid)

Trigger: Rule R0042 — promo_pro_when_engaged срабатывает когда subject входит в cohort high_engagement_free:

subject.tier = 'free' AND app_sessions_last_30d >= 10

⚡ Pulse-core dispatches show_ui_banner:

  • banner_id = promo_pro_upgrade_q2_2026
  • template_id = banner.upgrade_to_pro, template_version = 3
  • placement = top
  • dismissible = true
  • template_params = { discount_pct: "30", expires_in_days: "7" }

App receives, validates template_id exists, renders via Handlebars (см. spec §7.8) с template content из pulse_curated.templates.

📥 Возможные последовательности:

  • best case: app.banner_shownapp.banner_clicked (CTA "Upgrade now") → app.banner_dismissed (cta_clicked).
  • dismiss: app.banner_shownapp.banner_dismissed (user_close).
  • timeout: app.banner_shownapp.banner_dismissed (timeout) после expires_at.

📊 metric banner_engagement_rate derived from these events.

🗃️ pulse_runtime.decisions row + pulse_runtime.action_acks row для tracking.


Акт 5 — First payment

User action: Кликнул CTA → checkout flow (handled by billing, не Pulse) → пейнул.

📥 billing.payment_received (source = billing)

  • order_id, amount_cents = 999, currency = USD, tier_purchased = pro

📥 app.tier_changed (source = billing)

  • old_tier = free, new_tier = pro, reason = purchase

📊 feature_store.tier = 'pro', paid_at = now; metric subject.has_paid = true.

Действие #1 — receipt: rule transactional_receipt dispatches send_transactional_email:

  • Bypass marketing.email consent (это requires_consent: ops)
  • template_id = email.payment_receipt
  • provider_id = sendgrid_transactional_main
  • subject = "Your receipt for Kontinuum Pro"
  • from_address = billing@kontinuum.cloud

📥 Дальше провайдерные events: app.email_delivered (webhook) → возможно app.email_opened (если разрешён marketing.email_engagement_tracking).

Действие #2 — welcome-pro: rule welcome_pro_user ждёт 24 часа (idempotency {subject_id}:welcome_pro:{tier_changed_day}), затем show_ui_banner в Pro settings inline ("Pro features tour"), placement = settings_inline.


Акт 6 — Marketing campaign opt-in

User action: В Settings → Privacy включает marketing.email.

📥 app.consent_changed (purpose: ops — это compliance evidence)

  • purpose_id = marketing.email, new_state = granted, source = ui

🗃️ pulse_curated.consent_grants — append row; consent_current materialized view обновляется при refresh.

Через несколько дней rule q2_upgrade_campaign (на cohort installed_but_unpaid или recently_purchased) запускает send_marketing_email:

  • requires_consent: marketing.email
  • Subject в Asia/Tokyo? idempotency_key.send_day использует local day, не UTC → email приходит в 9 утра local time (см. spec §7.9.5).
  • При rate-limit hit → 📥 pulse_internal.rate_limit_hit + decision suppressed.

📥 lifecycle events: app.email_delivered, app.email_opened, app.email_clicked.


Акт 7 — User unsubscribes

User action: Кликает unsubscribe в footer email.

Provider returns webhook → pulse-core:

📥 app.email_unsubscribed (purpose: ops)

  • unsubscribe_method = footer_link

📥 app.consent_changed

  • purpose_id = marketing.email, new_state = revoked, source = system

🗃️ pulse_curated.consent_grants — revoke row.

⚡ Дальнейшие send_marketing_email для этого subject auto-suppress'аются (pulse_internal.decision_suppressed.suppression_reason = consent_missing).


Акт 8 — GDPR right-to-access request

User action: Settings → Privacy → "Export my data".

App calls POST /gdpr/access с subject auth token.

🗃️ pulse_runtime.gdpr_jobs — new row, kind = access, status = pending.

pulse-core-gdpr worker:

  1. Resolves identity-mapping (resolve_subject op).
  2. Aggregates from feature_store, events_summary (counts per event_type), decisions, outbound_events, consent_grants, audit_log.
  3. Generates Merkle proof для audit_records subset.
  4. Serializes как GdprAccessBundle (см. openapi.yaml).
  5. Uploads bundle to signed URL.

📥 pulse_internal.gdpr_job_completed (purpose: meta, retention 5 лет)

  • job_kind = access, status = completed, duration_ms

🗃️ pulse_runtime.audit_log — entry gdpr.access с actor_kind = subject.


Акт 9 — User уходит (право-на-забвение)

User action: "Delete my account" в Privacy settings.

App calls POST /gdpr/erasure.

🗃️ gdpr_jobs(kind=erasure) queued. pulse-core-gdpr worker (≤24 h SLA):

  1. identity_mapping.erase_subject — mapping marked erased_at.
  2. pulse-retention-worker (erasure mode) deletes rows from events, decisions, outbound_events, feature_store matching subject_id (через index, не partition drop).
  3. audit_log НЕ модифицируется — subject_id остаётся как «висящий псевдоним», но не resolvable.
  4. Pulse-client SDK на пользовательском устройстве — отдельная responsibility (App handles local wipe).

📥 pulse_internal.gdpr_job_completed (kind=erasure).

🗃️ audit_loggdpr.erase entry.

📥 Per spec §10.8.5: data.partition_dropped audit entries записываются при следующем drop'е партиций, где этот subject_id участвовал.


Cross-cutting инварианты

В любой момент:

  1. Consent gating — ни одно событие не попадает в pulse_runtime.events без валидного consent token для его purpose (§9.1.4-bis).
  2. Idempotency — никакое action не диспетчится дважды для одного idempotency_key (§7.9.5).
  3. Audit chain — каждая admin/compliance операция → row в pulse_runtime.audit_log с Merkle-hash chain (§9.3 + §10.6).
  4. Identity isolation — pulse-core НИКОГДА не видит raw identity_id. Только subject_id через identity-mapping (§9.1).
  5. Privacy invariant 11 — субjект NEVER targeted hard'ом по archetype в applies_when (§9.2.6).
  6. Retention — данные субъекта живут не дольше per-event retention_days (§10.8).
  7. Federation drift — если central и edge видят разные content-hashes контракта, ingest деградирует, не падает silently (§11).

Что НЕ покрыто M0

Из этого journey не часть M0:

  • DSL grammar (правила вручную через Directus admin → YAML, без формального parser'а).
  • ML inference (никаких ml.* лookup в rules).
  • LLM Gateway (никаких генерируемых текстов; только pre-approved templates).
  • A/B experiments (pulse_curated.experiments table есть, ассайнмент на client-side hash-split в M1+).
  • Cohort sync to external CDP (Customer.io / Iterable — M2+).
  • Survey administration (M2+).
  • Marketing patterns library (pulse_curated.marketing_patterns table seeded, но constraints validator — M3+).

См. полный M0-M5 roadmap в specification.md §15.


Полезные ссылки