← AstroMatrix Reports — all pages What AstroMatrix Knows Repository audit · astromatrix-flutter What AstroMatrix Knows 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. Written 2026-09-11 Branch new-ui Scope 454 Dart files + astromatrix-nodejs schemas & AI routes Changes made none 00 The stock take 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. Working Explicit facts Birth data, Big Three, 60+ chart and display preferences, language, selected partner — all persisted and synced. One critical fact is collected and discarded. Built, never read Behavioural signals users.stats accumulates per-screen visit counts and dwell averages for every user, server-side, surviving reinstall. No product surface has ever read it. Working Saved & intentional acts The Journal is already a timestamped, per-account, free-text-annotated store of everything a user deliberately kept. Rich. Under-modelled. 1 of 14 domains Derived patterns Tarot stats prove the aggregation mechanism works end to end — and cover the one domain where the dimension was pre-classified at write time. Not wired AI memory A complete conversational-memory schema, CrucibleChat, exists in Node against the same Mongo. Zero Dart files reference it. 01 Five headline findings Ordered by value of the thing being lost, not by effort to fix. 1 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. 2 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. 3 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”. 4 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. 5 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. 02 The ledger Every row below was verified in source. Persistent means it survives app restart; AI means it reaches an AI prompt today. Identity & astrology — well covered | 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. Behaviour — a real store exists, and nothing reads it | 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 03 Where the granularity goes 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. Choke 1 · The route already carries the answer, and drops it /component?component=Venus+square+Saturn&category=Intimacy+%26+Romance ↓  _sanitize() splits on ? and keeps the head — screen_timer_service.dart:165–166, mirrored in metrics_service.dart:326 "component" 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. Choke 2 · Prometheus cannot carry a person 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. Choke 3 · The one content metric carries category only 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. 04 What reaches the AI today Three AI endpoints exist. Two are reachable from the app, and the third is only used for translation. Path 1 · Chart reports ReportService.generateReport → AstroApiService POST /ai/calculatereport report_service.dart:43 · astro_api_service.dart:624 Language: wired { reportType, chart: , profileId, language } // profileId is sent and never used // no history, no prefs, no people Path 2 · Matrix reports — the main AI surface ReportService.generateMatrixReport POST /ai/generateDataReport report_service.dart:391 · routes/ai.js:497 Language: absent both sides { data: { category, view, matrixType, items[]: component, facet, aspect, sign, concept, opportunity, pattern, begins, ends, percentComplete, orb }, metadata: { userName, reportName, … } } // specificQuestion IS supported server-side // and no Flutter code ever sets it Path 3 · GPTService Generic completion, one caller: LanguageService, for translation gpt_service.dart:140 · language_service.dart:817, 944, 1102 Not a user surface { prompt, systemMessage, temperature, maxTokens } // stateless by construction Three specifics worth naming No conversation exists. No chat surface, no follow-up, no history. specificQuestion is plumbed end-to-end in Node and unreachable from the client — the “ask about this reading” capability is built and unwired. AI history is a file, not a record. 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. A working memory schema is one repo away. 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. 05 The model: facts, signals, inference 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 weights — intentional acts outrank passive ones | 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. An inference is only ever written with its receipts { "statement": "Relationships appear to be a current area of focus", "confidence": 0.72, "timescale": "current", "supportingSignals": [ { "dimension": "category:Intimacy & Romance", "score": 41.2, "events": 18 }, { "dimension": "person:p_8f2a", "score": 22.0, "events": 9 }, { "dimension": "chartType:chartrelationship", "score": 12.5, "events": 5 } ], "aiEligible": true, "userVisibleAs": "You've been exploring relationship themes a lot lately" } 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 taxonomy already ships 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. 06 Retrieval, not ingestion The profile is the index, not the payload. Each request assembles a budgeted context from ranked candidates: relevance × recency × actionWeight. Always sent~600 tok Chart placements and current transits for the subject The screen the user is on Language, knowledge tier, length and terminology preference Retrieved, capped~1,200 tok People named or implied by the query ≤2 Journal entries matching the category or person ≤5 Prior AI reports on the same category or person, as stored 2-line summaries ≤2 Saved interpretations in the category, titles only ≤5 Derived patterns, confidence ≥0.6 and AI-eligible ≤3 Never sentat any budget Raw event logs · full journal text · unrelated categories Low-confidence inferences Anything the user has not consented to, or that touches a sensitive category 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. 07 Trust, and one thing to fix regardless 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. 08 What to do first 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. Persist the onboarding facet 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. Read stats.pagecounts Already 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. Stop stripping the query string 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. Write subjectPersonIds onto journal entries Additive 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. Harden journal access Do it before anything above enriches journal data. Fix the Matrix AI language gap Small, self-contained, and the last English-only AI surface in the product. Read the tarot questions 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. Then start scoring 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. Full audit with every file:line citation, the complete data structures, the event taxonomy, the privacy controls and the five-phase roadmap: docs/user-knowledge-audit-2026-09.md. Companion docs: built-but-disconnected-2026-09.md · strategy-reference-2026-09.md · product-strategy-2026-09.md.