Repository audit · astromatrix-flutter
A code-level stock-take of everything the product currently stores, observes or could derive about a person — and how much of a User Knowledge Profile is already sitting in production, unread.
The suspicion behind this audit was right, and stronger than expected. There is no need to build a memory system from zero — three of the five layers already exist as production data.
Birth data, Big Three, 60+ chart and display preferences, language, selected partner — all persisted and synced. One critical fact is collected and discarded.
users.stats accumulates per-screen visit counts and dwell averages for every user, server-side, surviving reinstall. No product surface has ever read it.
The Journal is already a timestamped, per-account, free-text-annotated store of everything a user deliberately kept. Rich. Under-modelled.
Tarot stats prove the aggregation mechanism works end to end — and cover the one domain where the dimension was pre-classified at write time.
A complete conversational-memory schema, CrucibleChat, exists in Node against the same Mongo. Zero Dart files reference it.
Ordered by value of the thing being lost, not by effort to fix.
The most valuable fact we collect is thrown away in the same frame it is collected. Onboarding step 2 asks “What do you need answers about?”, requires an answer to advance, and never persists it — not to disk, not to the server, not even as an analytics label. It has been that way since the step shipped in df13e1ab (2025-12-22); git log -S selectedFacet shows nothing has touched it since.
A per-user behavioural store already exists in production and is write-only. stats.pagecounts and stats.avgs accumulate on every screen change and sync to Mongo. The only read anywhere in the app increments a separate ads counter.
Behavioural granularity is destroyed at three specific choke points — all fixable, none architectural. We can answer “how many people opened a relationship interpretation this week”; we cannot answer “has this user been reading relationship content”.
The AI knows nothing that distinguishes a returning user from a first-timer with the same chart. The richest AI request in the product sends a chart, a list of transit items, and a first name.
The interest taxonomy we would need to build already exists — as the 14 Self-Matrix and 12 Relationship-Matrix categories, already rolled up to the five onboarding facets by a tested mapping.
Every row below was verified in source. Persistent means it survives app restart; AI means it reaches an AI prompt today.
| Knowledge | Stored where | Persistent | AI | Status |
|---|---|---|---|---|
| Name, birth date / time / town, lat / lng, timezonelib/models/user_profile.dart:8–47 → models/User.js:12–41 | users | yes | yes | Fact |
| Big Three — sign, degree, minutes, houselib/services/user_service.dart:1665–1734 | users.data | yes | no | Fact |
| Every saved person (“additional charts”)one users row per person, peer of the account's own | users | yes | when selected | Fact |
| Currently selected partner — the one durable user↔person link that existslib/services/horoscope_service.dart:401–410 | prefs + local | yes | synastry | Fact |
| House system, zodiac system, 3 orb settings, smart orbsusers.settings via PreferenceService | users.settings | yes | no | Fact |
| 14 asteroid / point toggles, aspect & display prefslib/constants/preference_keys.dart:55–95, 269–288 | users.settings | yes | no | Literacy signal, unused |
| Languagesent on /ai/calculatereport only — see §04 | users.settings | yes | partly | Half-wired |
| Declared area of interest — Love / Evolution / Abundance / …lib/views/user_wizard_screen.dart:365–470; never reaches saveUser() | nowhere | no | no | Discarded |
Every chart, aspect, transit, progression, synastry, composite, draconic and return is computed on demand and cached disposably. No computed-chart collection exists anywhere — the engine README still lists “Database integration” and “Caching layer” as Not Implemented.
| Signal | Written by | Stored where | Server | Status |
|---|---|---|---|---|
| Per-screen visit counts | ScreenTimerServicescreen_timer_service.dart:111–160 | users.stats.pagecounts | yes | Write-only |
| Per-screen average dwell seconds | running meanscreen_timer_service.dart:141–145 | users.stats.avgs | yes | Write-only |
| Ads seen | AdsServiceads_service.dart:974–980 | users.stats.ads_seen | yes | Write-only |
| Chart-type dwell, per type | chart_screen.dart:119, 205, 347, 882 | folds into pagecounts | yes | Write-only |
| Tarot sessions + per-deck dwell | TarotReadingSessionmetrics_service.dart:408–455 | pagecounts + Prometheus | yes | Write-only |
| Gating session count / view count | ContentGatingServicecontent_gating_service.dart:128–131 | local only | no | Device-bound |
| Which interpretation, aspect, planet or house was opened | — | — | — | Not recorded |
| Which person a report was about | — | name inside a title string | — | Not recorded |
| Searches, AI questions, per-user notification opens, sessions | — | — | — | Not recorded |
Three choke points, each a deliberate past decision that was right for its own purpose and wrong for this one. The most consequential is a single line.
_sanitize() splits on ? and keeps the head — screen_timer_service.dart:165–166, mirrored in metrics_service.dart:326
The component name and its category are present in the route, at the call site, and are discarded one line later. We record that a user opened 200 interpretations; we never record which. This is the cheapest high-value fix in the document.
All metrics go to Prometheus counters and gauges. A session id is generated and deliberately left unattached — the comment at metrics_service.dart:30–52 spells out why: an unbounded label is one time series per session, a cardinality bomb. It ends: “It exists so session-scoped events can carry it once a real event store exists to receive them.” No such store was ever built. The Matrix events added for ISSUE-139 carry the same reasoning verbatim.
component_opened_total is labelled {category, source} — never the component name, never a user (component_summary.dart:350–356). Aggregate by construction. Firebase Analytics is not a second source: its whole surface is purchases, shares and four main-user diagnostics, it is disabled on web, and it joins to no user id we hold.
Three AI endpoints exist. Two are reachable from the app, and the third is only used for translation.
specificQuestion is plumbed end-to-end in Node and unreachable from the client — the “ask about this reading” capability is built and unwired.logAIInteraction appends to ai_log_YYYY_MM.json by rewriting the whole file, stores a first name and no user id, races across a clustered process, and has a user_feedback field that is always null. It is prompt QA, not memory.CrucibleChat defines per-user history with per-turn metadata, running stats, and a currentState block documented as “re-injected into next prompt”. It belongs to the quiz funnel, not the app — but it is a reviewed design for exactly this layer, already running against the same Mongo.Defect found in passing
ISSUE-39/122 threaded language through five AI paths. /ai/generateDataReport is not one of them, on either side. Every Matrix category report — the main AI surface — is still English-only for every user regardless of their language setting. Narrower than ISSUE-122, and not covered by it.
The governing rule
Twelve relationship lookups support relationship_interest: high.
They never support has_relationship_problems: true.
Five layers, separated so an observation can never be promoted into a fact by accident. Facts and saved acts mostly exist already; signals need scoring; inference needs receipts and decay.
| Action | Weight | Why | |
|---|---|---|---|
| Journal comment written | 5.0 | Effort, in the user's own words | |
| Tarot question asked | 5.0 | An explicit statement of need — already captured today | |
| Report generated | 4.0 | Deliberate, costly, often gated | |
| Interpretation saved (heart) | 3.0 | Intentional keep — already a journal write | |
| Entry reopened later | 3.0 | Sustained, not momentary | |
| Person selected for a reading | 2.0 | Directed attention | |
| Interpretation opened | 1.0 | Passive | |
| Screen dwell > 20s | 0.5 | Corroboration only — a backgrounded app looks identical | |
| Screen opened | 0.2 | Navigation noise | |
| Interpretation un-saved | −3.0 | Negative signal; prevents stale interests |
Scored with a 30-day half-life for current interest and 180-day for long-term themes, damped by novelty — ten opens of one component means attachment to that item, not interest in its domain. Nothing is reportable below 3 events across ≥2 distinct days.
The whole knowledge document is derived and disposable: delete it and it rebuilds from journals, events and settings. That single property is what makes “reset my personalization” a one-line delete, and what makes changing the scoring rules safe to ship.
The interest dimensions the brief proposes already exist, twice over, as the 14 Self-Matrix categories — Path & Career, Intimacy & Romance, Security & Roots, Soul Path & Karma, Emotional & Mental Health and ten more — and the 12 Relationship-Matrix categories. They are already rolled up to the five onboarding facets by a tested mapping in component_image_helper.dart:247–273, so Intimacy & Romance → Love is the same vocabulary the discarded onboarding answer speaks.
The profile is the index, not the payload. Each request assembles a budgeted context from ranked candidates: relevance × recency × actionWeight.
Summarising at write time rather than read time is what keeps this flat: a prior report costs ~40 retrieval tokens instead of ~1,500. A fully personalised Matrix report grows from roughly 2,500 to 4,000 prompt tokens — about +60% on a gpt-4.1-mini call, a rounding error against the revenue those reports gate.
The design goal is that a user reading their own profile page thinks “yes, that's fair” — not “how do you know that?” Every stored claim carries its provenance, and inferred ones are dismissible in one tap, with the dismissal actually suppressing the inference rather than hiding it.
Forbidden outright: never infer or store sexual orientation, gender identity, religion, health or mental-health status, pregnancy, substance use, immigration status, financial distress, or the state of a relationship. These are exactly what astrology browsing tempts. Reading twelve “Intimacy & Romance” interpretations is a content preference. The permitted claim is always “you have been reading X”, never “you are Y”.
Fix before Phase 1 ships
[Details of an open security item are withheld from this public copy.] One data-access endpoint needs stronger authentication before journals are enriched.
Ordered by value ÷ effort, and specific to what already exists. Everything here is capture or repair — none of it depends on the model being right, and all of it makes the model possible.
One field, one line, today. A self-declared life priority collected from 100% of new users at the moment of highest intent, currently discarded between two function calls. Before any AI work it can order the home screen and pick the first Matrix tab.
stats.pagecountsAlready in Mongo for every user. One read-only query answers “which categories does this person actually spend time in” — no new capture, no migration, no client release.
One line in each of two files converts the most-used behavioural counter in the app from “opened the component screen 200 times” into real content history.
subjectPersonIds onto journal entriesAdditive to an existing Mixed field, so no migration. Unblocks the entire relationship model — and fixes the real bug where reopening a saved relationship report renders whoever the partner is now.
Do it before anything above enriches journal data.
Small, self-contained, and the last English-only AI surface in the product.
Already in Mongo, already indexed, and the only unmediated user language the product holds. Even a manual read of a few hundred would inform the taxonomy and the prompt more than any inference engine.
Phase 2 and beyond only after the capture gaps are closed.
Item 1 alone is arguably worth more than Phases 2–5 combined per unit of effort. It should not wait for a roadmap.