# Zero PII Customer Intelligence Parte 2: Profili, Segnali e Segmenti

<!--category-- Product, Privacy, Segmentation, C# -->
<datetime class="hidden">2025-12-26T20:00</datetime>

Dentro [Parte 1](/blog/zero-pii-customer-intelligence-part1) abbiamo coperto la filosofia e la panoramica delle serie. [Parte 1.1](/blog/zero-pii-customer-intelligence-part1-1) Abbiamo costruito il generatore di dati campione.

Ora costruiamo il sistema centrale. Questa parte si concentra su:

1. **Architettura del profilo Zero-PII** - Sessioni effimere contro profili persistenti
2. **Segnali e pesi** - Quello che seguiamo e come si accumula
3. **Definizioni del segmento** - Membri fuzzy con regole ponderate (l'evento principale)
4. **Anteprima pattern in uscita** - Pubblicazione affidabile degli eventi (dettagliata nella parte 3)

Questo è il **progetto campione** (`Mostlylucid.SegmentCommerce`) Una piccola demo di ecommerce che mostra i core pattern. È intenzionalmente semplificata per dimostrare chiaramente i concetti, ma include anche i reali pattern di infrastruttura (gestione dei messaggi basata sull'outbox, elaborazione di background job, indicizzazione JSONB) che si scalerebbe orizzontalmente nella produzione.

Pensate a questo come "modelli di produzione in un singolo-app fattore di forma." La stessa struttura di codice funziona sia che si sta eseguendo un processo o distribuendo attraverso i servizi.

[TOC]

## Scelte tecnologiche

Abbiamo scelto una **pila semplice e coesa** mantenere il campione chiaro durante la dimostrazione dei modelli di produzione.

### PostgreSQL + pgvector: un database

Invece di database vettoriali separati, code di messaggi e livelli di cache, usiamo **PostgreSQL**:

```mermaid
flowchart LR
    App[ASP.NET Core] --> PG[(PostgreSQL)]
    
    PG --> JSONB[JSONB<br/>interests]
    PG --> Vector[pgvector<br/>embeddings]
    PG --> Queue[Queue tables<br/>jobs, outbox]
    PG --> FTS[Full-text<br/>tsvector]
    
    style PG stroke:#2f9e44,stroke-width:3px
    style JSONB stroke:#1971c2,stroke-width:2px
    style Vector stroke:#1971c2,stroke-width:2px
    style Queue stroke:#1971c2,stroke-width:2px
    style FTS stroke:#1971c2,stroke-width:2px
```

- **JSONB** per schemi flessibili (nessun NoSQL separato)
- **pgvector** per l'incorporamento (nessun Qdrant/Pinecone)
- **ELENCO/NOTIFICA** per i posti di lavoro (nessun Redis/RabbitMQ)
- **tsvector** per testo completo (nessuna ricerca elastica)

Un pool di connessioni, un backup, uno schieramento.

### HTMX + Alpine.js: Server-Driven UI

```html
<!-- Instant search (no page reload) -->
<input 
    hx-get="/api/search" 
    hx-trigger="keyup changed delay:300ms" 
    hx-target="#results" />
```

- **HTMX**: Server renders partials (no JSON → templating)
- **Alpine.js**: Reattività senza un passo di costruzione
- **Miglioramento progressivo**: Funziona senza JS, meglio con esso

UX SPA-like con la semplicità del server-rendering.

### ASP.NET Core: Modelli distribuibili

App singola, ma scala dei modelli:

- **Posta in uscita** (Eventi DB) → swap per bus messaggi
- **Coda di lavoro** (LISTEN/NOTIFY) → swap per pool di lavoratori
- **Collettore di sessione** → swap per API separate

Inizia semplice. Distribuisci quando necessario.

### Quello che abbiamo evitato

- Nessun database vettoriale separato (pgvector è posizionato)
- Nessun framework JS (HTMX + Alpine è più semplice)
- Nessun microservizio ancora (i modelli funzionano in monolito o distribuiti)
- Nessun Docker Compose sprawl (un DB, una app)

## La sfida zero-PII

Tradizionale negozio di tracciamento degli utenti informazioni identificabili: nomi, e-mail, ID utente legati al comportamento. GDPR e norme sulla privacy rendono questo sempre più problematico. Il nostro approccio è diverso:

**Immagazziniamo schemi comportamentali, non identita'.**

```mermaid
flowchart LR
    subgraph "Traditional Approach"
        User1[User: john@email.com] --> Behavior1[Bought headphones]
        Behavior1 --> PII1[(PII Database)]
    end
    
    subgraph "Zero-PII Approach"
        User2[Anonymous Session] --> Behavior2[Bought headphones]
        Behavior2 --> Pattern[(Category: tech<br/>Signal weight: 1.0)]
    end
    
    style PII1 stroke:#c92a2a,stroke-width:3px
    style Pattern stroke:#2f9e44,stroke-width:3px
```

L'intuizione chiave: **Non c'e' bisogno di sapere chi e' qualcuno per sapere cosa gli interessa.**.

## Panoramica dell'architettura

Il sistema ha tre livelli principali:

```mermaid
flowchart TB
    Browser[Browser] --> Session[Session Collector]
    Session --> Profile[Persistent Profile]
    Profile --> Segments[Segment Service]
    
    Session -->|in-memory only| Cache[(IMemoryCache)]
    Profile -->|elevated signals| DB[(PostgreSQL + JSONB)]
    Segments -->|memberships| UI[Segment Explorer UI]
    
    style Session stroke:#1971c2,stroke-width:3px
    style Profile stroke:#2f9e44,stroke-width:3px
    style Segments stroke:#fab005,stroke-width:3px
    style Cache stroke:#e64980,stroke-width:2px
```

1. **Collezionista di sessione** - Cattura segnali comportamentali (visioni, click, aggiunge carrello) - **Solo in memoria**
2. **Profilo persistente** - Aumenta i segnali di alto valore, calcola gli interessi - **Database-backed**
3. **Servizio di segmento** - Valuta i profili contro le regole, assegna i membri

**Distinzione critica**: Le sessioni sono effimere. I profili sono persistenti. Questo non è un dettaglio di implementazione è una decisione di architettura della privacy.

## Profili di sessione: In-Memory rigoroso (LFU Cache)

Le sessioni sono **rigorosamente in memoria**. Vivono in `IMemoryCache` con scadenza scorrevole e **non toccare mai il database**.

Questo è un vincolo architettonico difficile: i dati di sessione non possono persistere. Raccoglie segnali durante una visita e sfratti dopo 30 minuti di inattività tramite la politica di cache LFU (Least Frequently Used) sotto pressione di memoria.

### SessioneProfilo: Il modello In-Memory

```csharp
// Mostlylucid.SegmentCommerce/Models/SessionProfile.cs
public class SessionProfile
{
    public string SessionKey { get; set; } = string.Empty;

    // Category interest scores: { "tech": 0.75, "fashion": 0.25 }
    public Dictionary<string, double> Interests { get; set; } = new();

    // Detailed signal counts: { "tech": { "product_view": 5, "add_to_cart": 1 } }
    public Dictionary<string, Dictionary<string, int>> Signals { get; set; } = new();

    // Products viewed this session
    public List<int> ViewedProducts { get; set; } = new();

    // Session context (device, referrer domain, time-of-day)
    public SessionContext? Context { get; set; }

    // Aggregates
    public double TotalWeight { get; set; }
    public int SignalCount { get; set; }
    public int PageViews { get; set; }
    public int ProductViews { get; set; }
    public int CartAdds { get; set; }

    // Timestamps
    public DateTime StartedAt { get; set; } = DateTime.UtcNow;
    public DateTime LastActivityAt { get; set; } = DateTime.UtcNow;

    // Link to persistent profile (if fingerprint resolved)
    public Guid? PersistentProfileId { get; set; }
}
```

### Archiviato in IMemoryCache (o IDistributedCache) con scadenza scorrevole

```csharp
// Mostlylucid.SegmentCommerce/Services/Profiles/SessionCollector.cs
_cache.Set(sessionKey, sessionProfile, new MemoryCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(30),
    Priority = CacheItemPriority.Normal, // LFU eviction under memory pressure
    
    // CRITICAL: Eviction callback decides whether to elevate to persistent profile
    PostEvictionCallbacks =
    {
        new PostEvictionCallbackRegistration
        {
            EvictionCallback = async (key, value, reason, state) =>
            {
                if (value is SessionProfile session && ShouldElevate(session))
                {
                    // Only NOW do we write to database (persistent profile)
                    await ElevateToProfileAsync(session);
                }
                // Otherwise: session is gone forever
            }
        }
    }
});
```

**Costrizioni dure:**

- **Zero persistenza accidentale**: Sessioni dal vivo **solo** in cache (IMemoryCache o IDistributedCache come Redis)
- **Sfratto LFU**: Sezioni a basso consumo sfrattato prima sotto pressione di memoria
- **Scadenza scorrevole**: 30 minuti dall'ultima attività
- **Evizione callback**: Ultima possibilità di elevare segnali di alto valore prima di perderli
- **Nessun recupero**: App riavvio = tutte le sessioni perse (a meno che non usi IDistributedCache, ma ancora effimero)

### Decisione di elevazione (su evizione)

```csharp
private bool ShouldElevate(SessionProfile session)
{
    // Elevate if:
    // - User added to cart (high intent)
    // - User completed purchase (conversion)
    // - Fingerprint was resolved (identity established)
    // - Session weight exceeds threshold (engaged visitor)
    
    return session.CartAdds > 0 
        || session.TotalWeight > 5.0 
        || session.PersistentProfileId.HasValue;
}
```

**Perche' l'evitazione e' stata richiamata?**

Questo è il **solo** punto sicuro per decidere la persistenza. Entro il momento in cui la cache sfratta la sessione:

1. Conosciamo la cronologia completa delle sessioni
2. Possiamo valutare l'impegno totale
3. Evitiamo di memorizzare sessioni di basso valore (vista singola pagina, rimbalzo)
4. Garantiamo che le sessioni non persistono accidentalmente

Se non eleviamo durante lo sgombero, la sessione è **se n'è andato per sempre**. Questo è il design.

### Contesto della sessione: cosa teniamo sotto controllo (in modo sicuro)

```csharp
public class SessionContext
{
    public string? DeviceType { get; set; }        // "mobile", "desktop"
    public string? EntryPath { get; set; }         // "/products/tech" (no query params)
    public string? ReferrerDomain { get; set; }    // "google.com" (domain only, not full URL)
    public string? TimeOfDay { get; set; }         // "morning", "afternoon"
    public string? DayType { get; set; }           // "weekday", "weekend"
}
```

Guarda cosa c'e'. **non** qui: indirizzi IP, interpreti, URL completi, pixel di monitoraggio. Catturamo **modelli di contesto**, informazioni non identificabili.

## Che cosa sono i segnali?

A **segnale** è un fatto comportamentale catturato durante un'azione utente. Invece di rintracciare "chi," tracciamo "quello che è successo."

Questo concetto deriva da [Segnali effimeri](/blog/ephemeral-signals)In questo sistema, le operazioni emettono segnali come: `"api.rate_limited"` oppure `"gateway.slow"` per coordinare il comportamento senza stretto accoppiamento.

Qui applichiamo lo stesso modello al comportamento dell'utente:

- **Vista prodotto** → `product_view` segnale (peso: 0,10)
- **Aggiungi al carrello** → `add_to_cart` segnale (peso: 0,35)
- **Acquisto** → `purchase` segnale (peso: 1,00)

I segnali sono **effimero** (espire con la sessione), **zero-PII** (nessuna identità), e **ponderato** (Intent-aware).

### Impronte digitali dei clienti (identificazione dei cookie Zero)

Per collegare le sessioni senza tracciare i cookie, utilizziamo **impronta digitale lato client**. Il browser calcola un hash da segnali (timezone, risoluzione dello schermo, Renderer WebGL, impronte digitali su tela) e invia **solo l'hashish** a `/api/fingerprint`.

Il server quindi **HMAC che hash** con una chiave segreta, che lo rende site-scoped e inutilizzabile altrove.

Questo codice è adattato dal mio [Progetto di rilevamento dei bot](/blog/botdetection-introduction), dove viene usato per identificare i raschiatori. Stessa tecnica, scopo diverso.

```javascript
// Mostlylucid.SegmentCommerce/ClientFingerprint/fingerprint.js
// Collect signals (browser capabilities, not PII)
var signals = [
    Intl.DateTimeFormat().resolvedOptions().timeZone,
    navigator.language,
    screen.width + 'x' + screen.height,
    // ... (see full code)
];

// Hash locally
var hash = hash(signals.join('|'));

// Send only the hash via sendBeacon
navigator.sendBeacon('/api/fingerprint', JSON.stringify({ h: hash }));
```

**lato server:**

```csharp
// Server HMACs the client hash with a secret key
var profileKey = HMACSHA256(clientHash + secretKey);
```

Ora abbiamo un identificatore stabile, mappato dal sito, senza cookies o storage locale. [fonte completa di impronte digitali.js](https://github.com/scottgal/mostlylucidweb/blob/main/Mostlylucid.SegmentCommerce/ClientFingerprint/fingerprint.js) (adattato da per lo più Lucid.botdetection).

## Tipi di segnale e pesi

Le diverse azioni hanno diversi livelli di intenti. **pesi di base**.

### Tipi di segnale: Gerarchia del peso

```csharp
// Mostlylucid.SegmentCommerce/Data/Entities/Profiles/SignalEntity.cs
public static class SignalTypes
{
    // Passive signals (low intent)
    public const string PageView = "page_view";              // 0.01
    public const string CategoryBrowse = "category_browse";  // 0.03
    public const string ProductImpression = "product_impression"; // 0.02

    // Active signals (medium intent)
    public const string ProductView = "product_view";        // 0.10
    public const string ProductClick = "product_click";      // 0.08
    public const string Search = "search";                   // 0.05

    // High-intent signals
    public const string AddToCart = "add_to_cart";           // 0.35
    public const string AddToWishlist = "add_to_wishlist";   // 0.25
    public const string ViewCart = "view_cart";              // 0.15
    public const string BeginCheckout = "begin_checkout";    // 0.40

    // Conversion signals (highest intent)
    public const string Purchase = "purchase";               // 1.00
    public const string Review = "review";                   // 0.60
    public const string Share = "share";                     // 0.50

    public static readonly Dictionary<string, double> BaseWeights = new()
    {
        { PageView, 0.01 },
        { ProductView, 0.10 },
        { AddToCart, 0.35 },
        { Purchase, 1.00 },
        // ... (see full code for complete list)
    };

    public static double GetBaseWeight(string signalType)
    {
        return BaseWeights.GetValueOrDefault(signalType, 0.05);
    }
}
```

**Perche' questo e' importante:**

- Una vista a pagina singola (`0.01`) non dominerà il segnale
- Aggiunta al carrello (`0.35`) è un forte segnale d'intenti
- Acquisto (`1.00`) è il segnale più forte

Questa gerarchia impedisce "drive-by browsing" di inquinare il profilo.

## SessioneCollector: Segnali di registrazione (Solo cache)

```csharp
// Mostlylucid.SegmentCommerce/Services/Profiles/SessionCollector.cs
public async Task<SessionProfile> RecordSignalAsync(
    SessionSignalInput input, CancellationToken ct = default)
{
    var sessionKey = input.SessionKey;
    
    // Get or create session FROM CACHE (never DB)
    var session = _cache.Get<SessionProfile>(sessionKey);
    
    if (session == null)
    {
        session = new SessionProfile
        {
            SessionKey = sessionKey,
            StartedAt = DateTime.UtcNow
        };
    }

    session.LastActivityAt = DateTime.UtcNow;

    var weight = input.Weight ?? SignalTypes.GetBaseWeight(input.SignalType);

    // Update in-memory aggregates
    session.TotalWeight += weight;
    session.SignalCount++;

    if (!string.IsNullOrEmpty(input.Category))
    {
        session.Interests.TryGetValue(input.Category, out var currentScore);
        session.Interests[input.Category] = currentScore + weight;
    }

    if (input.SignalType == SignalTypes.AddToCart)
    {
        session.CartAdds++;
    }

    // Put back in cache with sliding expiration
    _cache.Set(sessionKey, session, new MemoryCacheEntryOptions
    {
        SlidingExpiration = TimeSpan.FromMinutes(30),
        Priority = CacheItemPriority.Normal,
        PostEvictionCallbacks = { /* elevation callback */ }
    });

    return session;
}
```

**Veloce perché:**

- In-memoria pura (nessuna scrittura DB)
- Nessuna serializzazione (IMemoryCache)
- Nessuna chiamata di rete (cache locale)

## Profili persistenti: Segnali elevati

Quando una sessione mostra alto intento (cart aggiunge, acquista), eleviamo segnali ad un **profilo persistente**.

### PersistentProfileEntity: Il profilo a lungo termine

```csharp
// Mostlylucid.SegmentCommerce/Data/Entities/Profiles/PersistentProfileEntity.cs
[Table("persistent_profiles")]
public class PersistentProfileEntity
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required]
    [MaxLength(256)]
    public string ProfileKey { get; set; } = string.Empty;

    // How this profile is identified (Fingerprint, Cookie, Identity)
    public ProfileIdentificationMode IdentificationMode { get; set; }

    // Behavioral data (all JSONB)
    [Column("interests", TypeName = "jsonb")]
    public Dictionary<string, double> Interests { get; set; } = new();

    [Column("affinities", TypeName = "jsonb")]
    public Dictionary<string, double> Affinities { get; set; } = new();

    [Column("brand_affinities", TypeName = "jsonb")]
    public Dictionary<string, double> BrandAffinities { get; set; } = new();

    [Column("price_preferences", TypeName = "jsonb")]
    public PricePreferences? PricePreferences { get; set; }

    [Column("traits", TypeName = "jsonb")]
    public Dictionary<string, bool> Traits { get; set; } = new();

    // Computed segments
    public ProfileSegments Segments { get; set; } = ProfileSegments.None;

    [Column("llm_segments", TypeName = "jsonb")]
    public Dictionary<string, double>? LlmSegments { get; set; }

    // Vector embedding for similarity matching
    [Column("embedding", TypeName = "vector(384)")]
    public Vector? Embedding { get; set; }

    // Statistics
    public int TotalSessions { get; set; }
    public int TotalSignals { get; set; }
    public int TotalPurchases { get; set; }
    public int TotalCartAdds { get; set; }

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
```

**Ancora zero PII**:

- `ProfileKey` è un hash HMAC (non reversibile)
- `IdentificationMode` ci dice come è stato identificato (fingerprint/cookie/login)
- Tutti i dati sono **segnali comportamentali**, non informazioni personali

### Elevazione: Sessione → Profilo

```csharp
public async Task ElevateToProfileAsync(
    SessionProfileEntity session, 
    PersistentProfileEntity profile, 
    CancellationToken ct = default)
{
    if (session.IsElevated)
        return;

    // Merge interests (use higher value)
    foreach (var (category, score) in session.Interests)
    {
        if (!profile.Interests.ContainsKey(category) || 
            profile.Interests[category] < score)
        {
            profile.Interests[category] = score;
        }
    }

    // Update stats
    profile.TotalSessions++;
    profile.TotalSignals += session.SignalCount;
    profile.TotalCartAdds += session.CartAdds;
    profile.LastSeenAt = DateTime.UtcNow;
    profile.UpdatedAt = DateTime.UtcNow;

    // Mark session as elevated
    session.IsElevated = true;
    session.PersistentProfileId = profile.Id;

    // Clear segment cache (will be recomputed)
    profile.SegmentsComputedAt = null;
    profile.EmbeddingComputedAt = null;

    await _db.SaveChangesAsync(ct);
}
```

**Quando si verifica l'elevazione:**

- Dopo un carrello aggiungere (alto intento)
- Dopo un acquisto (conversione)
- Quando l'utente sceglie la persistenza (fingerprint/cookie/login)

## Definizioni del segmento: The Payoff

Abbiamo preso in giro segmenti per due parti. Ora consegniamo. **output attuabile** di tutti quei segnali di raccolta rispondono "che tipo di shopper è questo?"

### Perché Fuzzy Membership?

La segmentazione tradizionale è binaria: o sei nel segmento o non lo sei. Questo crea problemi:

```mermaid
flowchart LR
    subgraph "Binary Segmentation"
        Profile1[3 purchases] -->|"Threshold: 5"| Out[NOT High-Value]
        Profile2[5 purchases] -->|"Threshold: 5"| In[High-Value]
    end
```

Un cliente con 4 acquisti è trattato in modo identico a uno con 0. Questo è sbagliato.

**Segmentazione fuzzy** dà ad ogni profilo un punteggio (0-1):

| Acquisti | Binario | Punteggio sfocato |
|-----------|--------|-------------|
| 0 | No | 0 |
| 2 | No | 0.4 |
| 4 | No | 0.8 |
| 5+ | Sì | 1,0 |

Ora puoi personalizzare proporzionalmente: i clienti "quasi di alto valore" ricevono un trattamento leggermente diverso da "non vicino."

### Struttura di definizione del segmento

```csharp
// Mostlylucid.SegmentCommerce/Services/Segments/SegmentDefinition.cs
public class SegmentDefinition
{
    public string Id { get; set; }           // "tech-enthusiast"
    public string Name { get; set; }          // "Tech Enthusiasts"
    public string Description { get; set; }   // "Users with strong interest in technology"
    public string Icon { get; set; }          // "🔧"
    public string Color { get; set; }         // "#3b82f6"

    public List<SegmentRule> Rules { get; set; } = [];

    // How rules combine: All (AND), Any (OR), Weighted (sum)
    public RuleCombination Combination { get; set; } = RuleCombination.Weighted;

    // Minimum score to be "a member" (0-1)
    public double MembershipThreshold { get; set; } = 0.3;

    public List<string> Tags { get; set; } = [];  // For filtering/grouping
}
```

### Tipi di regola: Che cosa è possibile controllare

Ogni regola valuta una dimensione del profilo:

```csharp
public enum RuleType
{
    CategoryInterest,  // Check interests.tech, interests.fashion, etc.
    BrandAffinity,     // Check brandAffinities.Sony, brandAffinities.Nike, etc.
    PriceRange,        // Check price preferences (budget vs luxury)
    Trait,             // Check boolean traits (prefersDeals, browsesExtensively)
    Statistic,         // Check totalPurchases, totalSessions, totalCartAdds
    TagAffinity,       // Check affinities.gadgets, affinities.organic, etc.
    Recency,           // Check days since last activity
    Expression         // Custom expressions (advanced)
}
```

### Regola Operatori

```csharp
public enum RuleOperator
{
    GreaterThan,       // value > threshold
    GreaterOrEqual,    // value >= threshold
    LessThan,          // value < threshold
    LessOrEqual,       // value <= threshold
    Equal,             // value == threshold
    NotEqual,          // value != threshold
    Contains,          // for array/string checks
    Between,           // for ranges
    In, NotIn          // for set membership
}
```

### Regola Metodi di Combinazione

Come le regole multiple si combinano in un punteggio finale:

```csharp
public enum RuleCombination
{
    All,      // AND logic: Score = min(all rule scores). All rules must pass.
    Any,      // OR logic: Score = max(all rule scores). Any rule can pass.
    Weighted  // Weighted sum: Score = Σ(rule.weight × rule.score) / Σ(rule.weight)
}
```

**ponderato** è la maggior parte delle cose comuni, ti permette di dire "la categoria di interesse conta il 60%, la reputazione conta il 30%, l'affinità del marchio conta il 10%."

### Esempi di segmenti reali

Ecco i segmenti predefiniti del progetto campione:

#### 1. Gli appassionati di tecnologia

```csharp
new SegmentDefinition
{
    Id = "tech-enthusiast",
    Name = "Tech Enthusiasts",
    Description = "Users with strong interest in technology products",
    Icon = "🔧",
    Color = "#3b82f6",
    MembershipThreshold = 0.35,
    Rules =
    [
        new() { 
            Type = RuleType.CategoryInterest, 
            Field = "interests.tech", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 0.4, 
            Weight = 0.6,  // 60% of score
            Description = "Tech interest > 40%" 
        },
        new() { 
            Type = RuleType.TagAffinity, 
            Field = "affinities.gadgets", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 0.2, 
            Weight = 0.2,  // 20% of score
            Description = "Likes gadgets" 
        },
        new() { 
            Type = RuleType.TagAffinity, 
            Field = "affinities.electronics", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 0.2, 
            Weight = 0.2,  // 20% of score
            Description = "Likes electronics" 
        }
    ]
}
```

**Esempio di valutazione:**

- Il profilo ha `interests.tech = 0.72`, `affinities.gadgets = 0.31`, `affinities.electronics = 0.15`
- Regola 1: 0,72 < 0,4 → punteggio 1,0 (supera la soglia)
- Regola 2: 0,31 < 0,2 → punteggio 1,0 (supera la soglia)
- Regola 3: 0,15 < 0,2 → punteggio 0,75 (parziale: 0,15/0,2)
- Finale: (1.0×0.6 + 1.0×0.2 + 0.75×0.2) / 1.0 = **0.95**
- Strumenti di capitale primario di classe 1 di soggetti del settore finanziario in cui l'ente ha un investimento significativo **Membro con fiducia al 95%**

#### 2. Abbandoni del carrello

```csharp
new SegmentDefinition
{
    Id = "cart-abandoner",
    Name = "Cart Abandoners",
    Description = "Users who add items to cart but don't complete purchase",
    Icon = "🛒",
    Color = "#ef4444",
    MembershipThreshold = 0.4,
    Rules =
    [
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalCartAdds", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 3, 
            Weight = 0.5, 
            Description = "3+ cart adds" 
        },
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalPurchases", 
            Operator = RuleOperator.LessThan, 
            Value = 2, 
            Weight = 0.5, 
            Description = "Few purchases" 
        }
    ]
}
```

Questo cattura il "aggiunge al carrello ma non compra" modello perfetto per le campagne di recupero.

#### 3. Clienti di alto valore

```csharp
new SegmentDefinition
{
    Id = "high-value",
    Name = "High-Value Customers",
    Description = "Customers who make frequent purchases and spend above average",
    Icon = "💎",
    Color = "#8b5cf6",
    MembershipThreshold = 0.4,
    Rules =
    [
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalPurchases", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 3, 
            Weight = 0.4, 
            Description = "3+ purchases" 
        },
        new() { 
            Type = RuleType.PriceRange, 
            Field = "priceRange", 
            Value = "100-10000",  // High-end shoppers
            Weight = 0.3, 
            Description = "High price range" 
        },
        new() { 
            Type = RuleType.Recency, 
            Field = "lastSeen", 
            Operator = RuleOperator.LessThan, 
            Value = 30, 
            Weight = 0.3, 
            Description = "Active in last 30 days" 
        }
    ]
}
```

#### 4. Cacciatori d'azzardo

```csharp
new SegmentDefinition
{
    Id = "bargain-hunter",
    Name = "Bargain Hunters",
    Description = "Price-sensitive shoppers who love deals and discounts",
    Icon = "🏷️",
    Color = "#22c55e",
    MembershipThreshold = 0.3,
    Rules =
    [
        new() { 
            Type = RuleType.PriceRange, 
            Field = "priceRange", 
            Value = "0-75",  // Budget shoppers
            Weight = 0.5, 
            Description = "Low price preference" 
        },
        new() { 
            Type = RuleType.Trait, 
            Field = "traits.prefersDeals", 
            Value = true, 
            Weight = 0.3, 
            Description = "Prefers deals" 
        },
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalCartAdds", 
            Operator = RuleOperator.GreaterThan, 
            Value = 5, 
            Weight = 0.2, 
            Description = "Shops around" 
        }
    ]
}
```

### Tutti i segmenti predefiniti

Il campione comprende 10 segmenti che coprono modelli comuni di commercio elettronico:

| Segmento | Icona | Regole chiave | Caso d'uso |
|---------|------|-----------|----------|
| Clienti di alto valore | Acquisti 3+, spesa elevata, recente | Trattamento VIP, programmi di fedeltà |
|Entusiasti della tecnologia | Interesse della tecnologia, affinità del gadget |Raccomandazioni del prodotto tecnico |
| Moda avanti | Moda interesse, visite multiple | Consigli di stile |
|Cacciatori di derivazione |Fasce di prezzo basse, prediligono offerte |Notifiche di vendita |
| Nuovi Visitatori | Nuovi Visitatori | Sezioni ≤2, nessun acquisto | Offerte di primo acquisto |
| Carrello Abbandonatori | 3+ carrello aggiunge, pochi acquisti | E-mail di recupero |
|Entusiasti della casa | Interesse della casa, attività recente |Vendita a domicilio |Vendita a domicilio |Vendita a domicilio |Vendita a domicilio
| Fitness Active | Sport interest, health traits | Fitness product focus |
| Clienti leali | Acquisti 5+, 10+ sessioni | Conservazione, ricompense |
| Cercatori | Alti segnali, sfoglia ampiamente | Strumenti di confronto, informazioni dettagliate |

## SegmentService: Computing Memberships

La `SegmentService` valuta i profili rispetto a tutte le regole del segmento:

```csharp
// Mostlylucid.SegmentCommerce/Services/Segments/SegmentService.cs
public SegmentMembership EvaluateSegment(ProfileData profile, SegmentDefinition segment)
{
    var ruleScores = new List<RuleScore>();
    
    foreach (var rule in segment.Rules)
    {
        var (score, actualValue) = EvaluateRule(profile, rule);
        ruleScores.Add(new RuleScore
        {
            RuleDescription = rule.Description,
            Score = score,
            Weight = rule.Weight,
            ActualValue = actualValue  // For transparency
        });
    }

    // Combine based on segment's combination method
    double finalScore = segment.Combination switch
    {
        RuleCombination.All => ruleScores.Min(r => r.Score),
        RuleCombination.Any => ruleScores.Max(r => r.Score),
        RuleCombination.Weighted => ComputeWeightedScore(ruleScores),
        _ => 0
    };

    return new SegmentMembership
    {
        SegmentId = segment.Id,
        SegmentName = segment.Name,
        Score = Math.Round(finalScore, 3),
        IsMember = finalScore >= segment.MembershipThreshold,
        RuleScores = ruleScores,
        Confidence = score switch  // Human-readable
        {
            >= 0.8 => "Very High",
            >= 0.6 => "High",
            >= 0.4 => "Medium",
            >= 0.2 => "Low",
            _ => "Very Low"
        }
    };
}
```

### Spiegabilità integrata

Ogni risultato dell'adesione comprende: **valori effettivi** che ha portato al punteggio:

```csharp
// What the UI receives:
{
    "segmentId": "tech-enthusiast",
    "segmentName": "Tech Enthusiasts",
    "score": 0.95,
    "isMember": true,
    "confidence": "Very High",
    "ruleScores": [
        { "description": "Tech interest > 40%", "score": 1.0, "actualValue": "0.72" },
        { "description": "Likes gadgets", "score": 1.0, "actualValue": "0.31" },
        { "description": "Likes electronics", "score": 0.75, "actualValue": "0.15" }
    ]
}
```

Gli utenti possono **vedere esattamente perché** Questo è fondamentale per la trasparenza e il rispetto del GDPR.

## Flusso del segnale: End-to-End

Ecco come una vista di prodotto diventa un segmento di appartenenza:

```mermaid
sequenceDiagram
    participant Browser
    participant Cache as SessionCache
    participant Outbox as Outbox
    participant Segment as SegmentService

    Browser->>Cache: Product view (category: "tech")
    Cache->>Cache: Update in-memory session
    Note over Cache: interests.tech += 0.10
    
    Browser->>Cache: Add to cart (high intent)
    Cache->>Outbox: Publish elevation event
    Outbox->>Outbox: Write to outbox table
    
    Note over Outbox: Background worker processes
    Outbox->>Segment: Elevate to PersistentProfile
    
    Segment->>Segment: Evaluate segment rules
    Note over Segment: interests.tech: 0.72 >= 0.40 ✓<br/>Score: 0.95, IsMember: true
    Segment-->>Browser: Segment memberships + explanations
```

## Il motivo Posta in uscita (anteprima)

Tutte le azioni significative scorrono attraverso il **Schema outbox**Il meccanismo di orchestrazione primaria:

```mermaid
flowchart LR
    Action[Cart Add] --> TX[Single Transaction]
    TX --> DB[(Save + Outbox)]
    DB --> Worker[Background Worker]
    Worker --> Route[Route to Handlers]
    
    style TX stroke:#2f9e44,stroke-width:3px
```

**Perché?** I dati aziendali e gli eventi sono scritti in una singola transazione. Gli eventi non possono essere persi. I fallimenti riprovano automaticamente con il backoff esponenziale.

```csharp
// Every action publishes to outbox in the same transaction
await using var transaction = await _db.Database.BeginTransactionAsync(ct);

cart.Items.Add(new CartItem { ProductId = productId });
await _db.SaveChangesAsync(ct);

_outbox.Publish(OutboxEventTypes.ProductAddedToCart, new { ProductId = productId });
await _db.SaveChangesAsync(ct);

await transaction.CommitAsync(ct);
// Event is now guaranteed to be processed
```

**Parte 3** copre l'implementazione full outbox: la coda di lavoro, LISTEN/NOTIFY per il pickup istantaneo, riprova la logica e i modelli di scala.

## Cosa c'e' dopo?

Questa parte riguardava:

- **Architettura del profilo Zero-PII** (sessioni efeme contro profili persistenti)
- **Definizioni del segmento** con l'adesione sfocata e le regole ponderate
- **Il servizio di segmento** con spiegabile incorporato

**Parte 3** va più a fondo in:

- **Implementazione schema in uscita** - pubblicazione di eventi affidabili e router singolo
- **Coda di lavoro** con PostgreSQL `SKIP LOCKED` per il trattamento distribuito
- **ELENCO/NOTIFICA** per il ritiro istantaneo del lavoro (nessun sondaggio)
- **UI e trasparenza** - il cruscotto "I tuoi interessi" e l'esploratore del segmento

---


*I segmenti sono l'uscita attivabile di questo sistema. Rispondono "che tipo di shopper è questo?" con punteggi fuzzy, non secchi binari. E ogni utente può vedere esattamente perché sono in un segmento.*