Momentum-beregning
Momentum er appens antifragile alternativ til streaks. Scoren (0–100) degraderer gradvis ved inaktivitet istedenfor å resette til null. Systemet belønner at du er her nå, ikke at du aldri tok en pause.
Arkitektur
heading.anchorLabelBeregningen lever i én delt modul:
apps/api/src/lib/momentum.ts ← kilde├── routes/dashboard/home.ts (konsument — hjemskjerm)└── queues/insights/insights-helpers.ts (konsument — regen-worker)Hjemskjerm-endepunktet cacher resultatet via getOrSetCache(CacheKey.home(...)).
Aktivitetskilder
heading.anchorLabelMomentum teller aktivitet fra alle 4 brukerhandlinger:
| Kilde | Tabell | Dato-felt | Hva den fanger |
|---|---|---|---|
| Impulser | impulses | created_at | Impulsfangst |
| Sjekk-inn | check_ins | created_at | ”Hva gjør du nå?”-refleksjoner |
| Sesjoner | sessions | last_activity_at | Steg-aktivitet i frigjøring/manifestasjon |
| Notater | notes | created_at | Refleksjoner og journalføring |
Dette sikrer at all brukerengasjement gir momentum, ikke bare impulsfangst.
Beregningsformel
heading.anchorLabelScore = Recent Activity (0-50) + Cycle Progress (0-30) + Consistency (0-20)Komponent 1: Recent Activity (50%)
heading.anchorLabelAktivitet siste 7 dager, vektet mot nyere dager. Maks én telling per dag — volum gir ikke ekstra score.
For hver unike aktive dag siste 7 dager: vekt = 1.0 - (dagerSiden × 0.1)
Dag 0 (i dag) = 1.0 Dag 1 (i går) = 0.9 Dag 2 = 0.8 ... Dag 6 = 0.4
sum = Σ(vekter for unike aktive dager)maxScore = 4.9 (1.0 + 0.9 + ... + 0.4)recentComponent = min(50, (sum / 4.9) × 50)Egenskaper:
- Aktiv hver dag i 7 dager = 50 poeng (maks)
- Kun aktiv i dag = ~10 poeng
- Ingen aktivitet siste 7 dager = 0 poeng
- Flere aktiviteter samme dag teller ikke ekstra — forhindrer volum-gaming
Decay-rate: Uten ny aktivitet mister du ~0.1 vektpoeng per dag. Etter 7 dager inaktivitet = 0.
Komponent 2: Cycle Progress (30%)
heading.anchorLabelAndel impulser som har blitt prosessert gjennom sesjoner, siste 30 dager.
completionRate = recentCompletedSessions / recentImpulses (siste 30 dager)cycleComponent = min(30, completionRate × 30)| Sesjoner (30d) | Impulser (30d) | Rate | Poeng |
|---|---|---|---|
| 10 | 10 | 100% | 30 |
| 5 | 10 | 50% | 15 |
| 5 | 50 | 10% | 3 |
| 0 | 20 | 0% | 0 |
30-dagers vinduet sikrer at scoren reflekterer nåværende praksis, ikke historisk gjeld.
Komponent 3: Consistency (20%)
heading.anchorLabelAntall unike dager med aktivitet siste 7 dager (fra alle 4 kilder).
consistencyComponent = (uniqueActiveDays / 7) × 20| Aktive dager | Poeng |
|---|---|
| 7/7 | 20 |
| 4/7 | ~11 |
| 1/7 | ~3 |
| 0/7 | 0 |
Nivågrenser
heading.anchorLabel| Score | Nivå | Ikon | Beskrivelse |
|---|---|---|---|
| 80–100 | Igniting | flame.fill | Your practice carries its own force |
| 60–79 | Flowing | wind | You’re finding your rhythm |
| 40–59 | Steady | circle.fill | A quiet strength is forming |
| 0–39 | Stirring | arrow.up.circle | Something is starting to move |
API returnerer nivået som streng i transformation_momentum.momentum_level.
Scenarioanalyse
heading.anchorLabel| Brukertype | Recent | Cycle | Consist | Total | Nivå |
|---|---|---|---|---|---|
| Daglig, prosesserer alt | 50 | 30 | 20 | 100 | Igniting |
| Daglig, halvparten prosessert | 50 | 15 | 20 | 85 | Igniting |
| 4 dager/uke, prosesserer alt | ~33 | 30 | ~11 | ~74 | Flowing |
| Kun sjekk-inn daglig (ingen impulser) | 50 | 0 | 20 | 70 | Flowing |
| Én impuls i dag, ny bruker | ~10 | 0 | ~3 | ~13 | Stirring |
| Var aktiv, 3 dager pause | ~19 | varierer | ~6 | ~25–45 | Stirring/Steady |
| Var aktiv, 7 dager pause | 0 | varierer | 0 | 0–30 | Stirring |
Robusthet
heading.anchorLabel| Scenario | Oppførsel | Status |
|---|---|---|
| Ny bruker, 0 aktivitet | score=0, level=“stirring” | OK |
Tom activityDates array | Returnerer 0 umiddelbart | OK |
| DB-feil på activity-queries | Logget som warning, tom array, score=0 | Graceful |
| DB-feil på stats/impulses | Kaster feil, 500-respons | Korrekt |
| Fremtidig dato (klokkedrift) | daysAgo < 0, filtrert ut | OK |
completedSessions > impulses (30d) | Rate > 1.0, capped av Math.min(30) | OK |
| 50 aktiviteter på én dag | Teller som 1 dag (volum-cap) | OK |
latestInsight.content er null | Returnerer tom streng | OK |
| Concurrent requests | Cache forhindrer dobbeltberegning | OK |
Kjente begrensninger
heading.anchorLabel- Server-tidssone. Dager beregnes fra serverens midnatt (
setHours(0,0,0,0)), ikke brukerens lokale tid. Akseptabelt for nåværende nøyaktighetsnivå.
Dataflyt
heading.anchorLabelGET /api/v1/dashboard/home → getOrSetCache(CacheKey.home(userId)) → fetchHomeData(userId) → Promise.all([ impulseActivity, checkInActivity, sessionActivity, noteActivity, (14 dager) recentCompleted, recentImpulses, (30 dager) impulses, stats, insight, pulse (UI-data) ]) → calculateMomentumScore(allActivityDates, recentCycles, recentImpulses) → getMomentumLevel(score)Cache invalideres av regen-worker etter nye impulser/sesjoner.
API-respons
heading.anchorLabel{ "transformation_momentum": { "momentum_score": 42, "momentum_level": "steady", "current_cycle": { "captured": 3, "transforming": 1, "transformed": 2 }, "cycles_completed": 12, "latest_insight": { "id": "uuid", "title": "pattern", "preview": "Du viser et mønster..." } }}Klientintegrasjon
heading.anchorLabel| Plattform | Fil | Tilgang |
|---|---|---|
| KMP | HomeResponse.kt → TransformationMomentum | momentumScore, momentumLevel |
| iOS | HomeStoreWrapper.swift | homeStore.momentumScore, homeStore.momentumLevel |
| Android | HomeScreen.kt | Via Compose state fra KMP store |