Grand Diomande Research · Full HTML Reader

Serenity Soother — Affinity Graph Architecture

**Document ID:** SS-ARCH-002 **Version:** 1.0.0 **Last Updated:** 2026-01-15 **Source:** `Desktop/SS/SerenitySoother/SerenitySoother/Core/Infrastructure/AffinityGraph.swift`

Research Backlog architecture technical paper candidate score 44 .md

Full Public Reader

Serenity Soother — Affinity Graph Architecture

Document ID: SS-ARCH-002
Version: 1.0.0
Last Updated: 2026-01-15
Source: `Desktop/SS/SerenitySoother/SerenitySoother/Core/Infrastructure/AffinityGraph.swift`

---

Overview

A living graph of user relationships that evolves as users create and interact. Uses Locality-Sensitive Hashing (LSH) for O(1) matching at scale.

---

Connection Types

swift
enum ConnectionType: String, Codable {
    case soulMirror = "SOUL_MIRROR"         // 95-100%
    case kindredSpirit = "KINDRED_SPIRIT"   // 85-95%
    case resonantTraveler = "RESONANT"      // 75-85%
    case parallelPath = "PARALLEL"          // 70-75%
    case distantEcho = "DISTANT"            // <70%
}
TypeAffinityBehavior
Soul Mirror95-100
Kindred Spirit85-95
Resonant Traveler75-85
Parallel Path70-75
Distant Echo<70

---

5-Dimensional Affinity Calculation

swift
enum AffinityDimension {
    case therapeuticGoals      // 40%
    case realmPreferences      // 25%
    case objectInteractions    // 20%
    case sessionPatterns       // 10%
    case emotionalValence      // 5%
}

Formula

Soul Affinity Score = (
    Therapeutic Goals       × 0.40
  + Realm Preferences       × 0.25
  + Object Interactions     × 0.20
  + Session Patterns        × 0.10
  + Emotional Valence       × 0.05
)

---

Affinity Edge Structure

swift
struct AffinityEdge: Identifiable, Codable {
    let id: UUID
    let fromUserId: UUID
    let toUserId: UUID
    let score: Float                        // 0.0 - 1.0
    let connectionType: ConnectionType
    let sharedDimensions: [AffinityDimension]
    let lastCalculatedAt: Date
    var mutualContributions: Int

    var isExpired: Bool {
        Date().timeIntervalSince(lastCalculatedAt) > 3600 * 24
    }
}

---

LSH Hashing for O(1) Lookup

### Problem
Naive O(n²) comparison doesn't scale:
- 1M users = 500 billion comparisons

Solution: Locality-Sensitive Hashing

swift
struct UserAffinityHash: Codable {
    let userId: UUID
    let therapeuticBucket: String    // "anxiety-sleep-stress"
    let realmBucket: String          // "ocean-aurora-forest"
    let objectBucket: String         // "water-moon-tree"
    let sessionBucket: String        // "evening-long-frequent"
    let compositeHash: String        // Combined for quick lookup
}

Bucket Creation

swift
static func from(profile: UserAffinityProfile) -> UserAffinityHash {
    // Therapeutic bucket: top 3 goals, sorted, joined
    let therapeuticBucket = profile.therapeuticGoals
        .prefix(3)
        .map { $0.rawValue.lowercased() }
        .sorted()
        .joined(separator: "-")

    // Realm bucket: top 3 preferred realms
    let realmBucket = profile.realmPreferences
        .sorted { $0.value > $1.value }
        .prefix(3)
        .map { $0.key.rawValue }
        .sorted()
        .joined(separator: "-")

    // Session bucket: time + duration + frequency
    let timeSlot = profile.preferredHour < 12 ? "morning" :
                   profile.preferredHour < 17 ? "afternoon" :
                   profile.preferredHour < 21 ? "evening" : "night"
    let durationSlot = profile.avgDuration < 10 ? "short" :
                       profile.avgDuration < 20 ? "medium" : "long"
    let freqSlot = profile.sessionsPerWeek < 3 ? "occasional" :
                   profile.sessionsPerWeek < 7 ? "regular" : "frequent"

    return UserAffinityHash(
        therapeuticBucket: therapeuticBucket,
        realmBucket: realmBucket,
        compositeHash: "\(therapeuticBucket)|\(realmBucket)"
    )
}

---

Bucket Management

swift
func addToBucket(hash: UserAffinityHash) {
    if buckets[hash.compositeHash] == nil {
        buckets[hash.compositeHash] = []
    }
    buckets[hash.compositeHash]?.insert(hash.userId)

    // Check if bucket needs splitting
    if bucket.count > bucketThreshold {
        splitBucket(hash.compositeHash)
    }
}

### Configuration
- `bucketThreshold`: 1000 users per bucket
- `edgeCacheDuration`: 1 hour

---

Finding Candidates (O(1))

swift
func findCandidates(for profile: UserAffinityProfile, limit: Int = 100) -> [UUID] {
    let hash = UserAffinityHash.from(profile: profile)

    var candidates: Set<UUID> = []

    // Primary bucket
    if let primary = buckets[hash.compositeHash] {
        candidates.formUnion(primary)
    }

    // Adjacent buckets (fuzzy matching)
    let adjacentKeys = generateAdjacentBucketKeys(hash)
    for key in adjacentKeys {
        if let adjacent = buckets[key] {
            candidates.formUnion(adjacent)
        }
    }

    candidates.remove(profile.userId)
    return Array(candidates.prefix(limit))
}

---

Graph Queries

Find Neighbors

swift
func neighbors(of userId: UUID, minAffinity: Float = 0.7, limit: Int = 50) -> [AffinityEdge]

Path Exists (BFS)

swift
func pathExists(from: UUID, to: UUID, maxHops: Int = 3, minAffinity: Float = 0.7) -> Bool

Find Transitive Path

swift
func findTransitivePath(from: UUID, to: UUID, maxHops: Int = 3, minAffinity: Float = 0.7) -> [AffinityEdge]

---

Attribution Chain

Tracks how content flows through the network:

swift
struct AttributionChain: Codable {
    let originalCreatorId: UUID
    let originalCreatorName: String
    let hops: [AttributionHop]

    struct AttributionHop: Codable {
        let userId: UUID
        let userName: String
        let affinity: Float
        let hopNumber: Int
    }

    func displayText() -> String {
        // "Created by Peaceful Wanderer #4721,
        //  discovered through Serene Spirit #2891,
        //  passed on by Tranquil Seeker #8234"
    }
}

---

Network Statistics

swift
struct NetworkStatistics {
    let totalUsers: Int
    let totalEdges: Int
    let averageAffinity: Float
    let averageClusterSize: Float
    let soulMirrorPairs: Int
    let kindredSpiritPairs: Int
}

---

Storage (SwiftData)

swift
@Model
final class StoredAffinityEdge {
    var id: UUID
    var fromUserId: UUID
    var toUserId: UUID
    var score: Float
    var connectionTypeRaw: String
    var sharedDimensionsRaw: String
    var lastCalculatedAt: Date
    var mutualContributions: Int
}

---

Batch Recalculation

swift
func recalculateAffinity(
    for profile: UserAffinityProfile,
    allProfiles: [UserAffinityProfile],
    modelContext: ModelContext
) {
    // 1. Find candidates via LSH (O(1))
    let candidates = findCandidates(for: profile, limit: 100)

    // 2. Only calculate affinity for candidates
    let candidateProfiles = allProfiles.filter { candidates.contains($0.userId) }

    // 3. Persist significant edges
    for otherProfile in candidateProfiles {
        let edge = calculateAffinity(between: profile, and: otherProfile)
        if edge.score >= 0.7 {
            persistEdge(edge, modelContext: modelContext)
        }
    }
}

---

Graph Visualization

                    ┌─────────┐
            ┌──────▶│ User A  │◀──────┐
            │       └────┬────┘       │
            │            │            │
   0.92     │     0.87   │   0.78     │  0.71
  (KS)      │     (KS)   │   (RT)     │  (PP)
            │            │            │
     ┌──────┴───┐  ┌─────┴─────┐  ┌───┴──────┐
     │ User B   │  │  User C   │  │ User D   │
     └────┬─────┘  └─────┬─────┘  └──────────┘
          │              │
          │     0.94     │
          │     (SM)     │
          └──────┬───────┘
                 │
                 ▼
          ┌──────────┐
          │ User E   │ ◀── Soul Mirror between B & C
          └──────────┘     B's creation → C & E automatically

---

Change Log

VersionDateChanges
1.0.02026-01-15Initial creation

Promotion Decision

Promote into a technical note or architecture paper with implementation anchors.

Source Anchor

spine/Serenity-Soother/architecture/AFFINITY_GRAPH.md

Detected Structure

Method · References · Code Anchors · Architecture