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. 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. 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. 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. 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. 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

KnowledgeStored wherePersistentAIStatus
Name, birth date / time / town, lat / lng, timezonelib/models/user_profile.dart:8–47 → models/User.js:12–41usersyesyesFact
Big Three — sign, degree, minutes, houselib/services/user_service.dart:1665–1734users.datayesnoFact
Every saved person (“additional charts”)one users row per person, peer of the account's ownusersyeswhen selectedFact
Currently selected partner — the one durable user↔person link that existslib/services/horoscope_service.dart:401–410prefs + localyessynastryFact
House system, zodiac system, 3 orb settings, smart orbsusers.settings via PreferenceServiceusers.settingsyesnoFact
14 asteroid / point toggles, aspect & display prefslib/constants/preference_keys.dart:55–95, 269–288users.settingsyesnoLiteracy signal, unused
Languagesent on /ai/calculatereport only — see §04users.settingsyespartlyHalf-wired
Declared area of interest — Love / Evolution / Abundance / …lib/views/user_wizard_screen.dart:365–470; never reaches saveUser()nowherenonoDiscarded

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

SignalWritten byStored whereServerStatus
Per-screen visit countsScreenTimerServicescreen_timer_service.dart:111–160users.stats.pagecountsyesWrite-only
Per-screen average dwell secondsrunning meanscreen_timer_service.dart:141–145users.stats.avgsyesWrite-only
Ads seenAdsServiceads_service.dart:974–980users.stats.ads_seenyesWrite-only
Chart-type dwell, per typechart_screen.dart:119, 205, 347, 882folds into pagecountsyesWrite-only
Tarot sessions + per-deck dwellTarotReadingSessionmetrics_service.dart:408–455pagecounts + PrometheusyesWrite-only
Gating session count / view countContentGatingServicecontent_gating_service.dart:128–131local onlynoDevice-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: <ChartPayload>, 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

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

ActionWeightWhy
Journal comment written5.0Effort, in the user's own words
Tarot question asked5.0An explicit statement of need — already captured today
Report generated4.0Deliberate, costly, often gated
Interpretation saved (heart)3.0Intentional keep — already a journal write
Entry reopened later3.0Sustained, not momentary
Person selected for a reading2.0Directed attention
Interpretation opened1.0Passive
Screen dwell > 20s0.5Corroboration only — a backgrounded app looks identical
Screen opened0.2Navigation noise
Interpretation un-saved−3.0Negative 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Harden journal access

    Do it before anything above enriches journal data.

  6. Fix the Matrix AI language gap

    Small, self-contained, and the last English-only AI surface in the product.

  7. 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.

  8. 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.