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`
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
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%
}| Type | Affinity | Behavior |
|---|---|---|
| Soul Mirror | 95-100 | |
| Kindred Spirit | 85-95 | |
| Resonant Traveler | 75-85 | |
| Parallel Path | 70-75 | |
| Distant Echo | <70 |
---
5-Dimensional Affinity Calculation
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
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
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
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
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))
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
func neighbors(of userId: UUID, minAffinity: Float = 0.7, limit: Int = 50) -> [AffinityEdge]Path Exists (BFS)
func pathExists(from: UUID, to: UUID, maxHops: Int = 3, minAffinity: Float = 0.7) -> BoolFind Transitive Path
func findTransitivePath(from: UUID, to: UUID, maxHops: Int = 3, minAffinity: Float = 0.7) -> [AffinityEdge]---
Attribution Chain
Tracks how content flows through the network:
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
struct NetworkStatistics {
let totalUsers: Int
let totalEdges: Int
let averageAffinity: Float
let averageClusterSize: Float
let soulMirrorPairs: Int
let kindredSpiritPairs: Int
}---
Storage (SwiftData)
@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
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
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2026-01-15 | Initial 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