A provenance + confidence fact store, people/patterns scaffolds, the EX app icon.
Date: 2026-06-13 (Session 8) Theme: Turn the flat user profile into a typed memory store where every fact remembers *where it came from* and *how sure we are* - the second, deeper step of collaborator Mete Selçuk Şimşek's 6-area memory model - and ship the selected EX app icon into the build pipeline toward TestFlight.
Session 7 wrote the *write-path* of memory (engine-off episode summarisation). Session 8 does the *structural* upgrade Mete asked for: a universal record {value, source, confidence, updated_at}, a driver/vehicle privacy split, and ready-but-empty scaffolds for the two areas that need hardware we do not have yet (diarization, gpsd).
Mete's memory proposal defines six areas - vehicle / fuel / driver / people / episodes / patterns - all following the same rule:
*no sensor → ask the driver at a natural moment → store with **source +
confidence** → speak by confidence → never fabricate.*
Session 7 delivered episodes (daily summaries). The blocker for the rest was that the profile had no place to *put* a source or a confidence: it was a flat {field: value} dict where the last writer won, so a shaky regex guess could silently overwrite something the driver stated. This session builds the typed substrate the other five areas hang off.
What. A new memory/facts.py: every stored fact is a FactRecord (value, source, confidence, updated_at). Source is one of USER (1.0), SENSOR (0.9), INFERRED (0.6) in descending trust.
Why. Without provenance there is no honest way to "speak by confidence" or to protect an explicit statement from a later guess. This is the precondition for every other memory area.
How. FactStore.set() runs a single, testable rule:
existing is None → accept
source is USER → accept (an explicit correction always wins)
new confidence >= existing → accept (never downgrade certainty)
otherwise → reject
So extract_profile_facts() (regex, INFERRED) can add new facts but can never clobber what the driver typed in the app (USER).
Purpose. Memory that gets *more* trustworthy over time instead of drifting on the last noisy write.
What. Facts are partitioned by Scope: DRIVER (PRIVATE → driver.json) and VEHICLE (SHARED → vehicle.json). A field→scope map routes each known field; unknown fields default to the more-private DRIVER.
Why. A car can be lent or sold. Vehicle facts (make, year, tank capacity) belong to the car and should travel with it; driver facts (name, city, taste) must not leak to the next driver. Mete's model calls these out as distinct areas; the privacy class makes that split structural, not a convention.
How. Two JSON files, one FactStore over both. flat() merges them back to {field: value} for any consumer that just wants the value.
Purpose. A future "vehicle handover" / "factory reset" is a file delete, not a field-by-field audit.
What. memory/people.py (PeopleStore) and memory/patterns.py (PatternStore), both reusing Source/confidence.
Why. Two of Mete's six areas depend on capabilities that aren't wired: people needs speaker diarization to bind a voice to a person; patterns needs gpsd traces to confirm a routine. Building the stores now means the day those land, it's a data-source swap, not a new subsystem.
How.
Person carries a voice_id that is None until diarization can fill it; the store already supports by_voice() lookup.
Pattern carries a schedule that is None until gpsd confirms it; an inferred routine is stored at INFERRED (0.6) and gets rewritten at SENSOR confidence once observed.
Purpose. The assistant can already remember "your wife Aylin" or "you usually drive mornings" from conversation, with confidence, and harden those facts later without a rewrite.
What. PersistentMemory now stores the profile in the FactStore but keeps its old public surface (get_profile, update_profile, extract_profile_facts) and still mirrors a flat user_profile.json.
Why. Two existing consumers must not break: the mobile /profile route (reads a flat dict) and the fuel estimator, which reads user_profile.json *directly* for tank capacity and consumption.
How.
update_profile() (explicit app edits) writes as USER; extract_profile_facts() writes as INFERRED.
_write_flat_projection() mirrors the typed store back to user_profile.jsonafter every write, so the fuel route keeps working untouched.
_migrate_legacy_profile() does a one-time import of any pre-existing flat profile into the typed store (seeded as USER, since it was driver-entered), running only while the typed store is still empty.
get_profile_records() + GET /profile/details expose per-field {value, source, confidence, updated_at} so the provenance is visible to the app.
Purpose. A structural upgrade with zero behaviour change for existing callers.
What. FuelState gained a confidence field, derived from its source (manual 1.0, obd 0.95, estimated 0.5). The Home screen fuel widget now appends "· estimated" when confidence is low.
Why. Mete's rule is "speak by confidence" - the same idea the driver should *see*: a measured tank-full reading and a model guess from odometer should not look identical.
How. _save_state()/get_fuel() set confidence from _SOURCE_CONFIDENCE; mobile FuelState and HomeScreen surface it.
Purpose. Honest UI - the range number carries its own certainty.
What. The selected EX monogram is now the app icon: docs/assets/icon-concepts/04-monogram-ex.png → mobile/assets/icon.png (1024×1024, no alpha) and adaptive-icon.png; app.json wires icon and the Android adaptiveIcon.foregroundImage.
Why. The icon was chosen in Session 7 but never placed; the app shipped with no icon set at all (only an adaptiveIcon.backgroundColor).
How. Confirmed the project is the managed/prebuild flow (ios/ is git-ignored), so app.json's icon is authoritative at EAS build time. Verified the source PNG is exactly 1024×1024 with no alpha channel (iOS rejects alpha-channel icons), and that expo config resolves both icon paths and the com.excar.app bundle id.
Handover (needs Selim). eas build -p ios --profile production and eas submit -p ios --profile production require Apple credential 2FA, so they are left as the one manual step - see *Open items*.
Purpose. Everything controllable for "icon → TestFlight" is done and verified; only the Apple-authenticated build/submit remains.
Symptom. The Profile screen lets the driver set Tank capacity and Avg consumption, but those values never stuck.
Root cause. The backend ProfileUpdate Pydantic model (routes/profile.py) did not declare tank_capacity_l or avg_consumption_l_per_100km, and _PROFILE_DEFAULTS did not list them either. The mobile app PATCHed them; the model dropped the unknown fields; get_profile() never returned them - a silent data loss across the exact two fields the fuel estimator depends on.
Fix. Added both fields to ProfileUpdate and to _PROFILE_DEFAULTS, so they now round-trip through the typed store and the flat projection the fuel route reads. Covered by test_flat_projection_written_for_legacy_readers.
17 new tests, 54 passing (was 37):
test_facts.py - conflict resolution (inference can't overwrite user; user always wins; equal/higher confidence overwrites), scope-split file routing, provenance, persistence round-trip, import_flat, flat.
test_people_patterns.py - people upsert/merge, by_voice, persistence; pattern default confidence + gpsd-ready schedule.
test_profile_store.py - profile round-trip, explicit edit surviving laterinference, flat projection file, legacy migration, confidence exposure.
New modules are ruff + mypy --strict clean.
New facts.py module + backward-compatible projection, not an in-place rewrite. A hard cutover of persistent.py would have broken the fuel estimator, which reads user_profile.json directly. Keeping the flat projection means the typed store is the source of truth while legacy readers see no change. *Rejected:* migrating the fuel route to the typed store this session - more blast radius for no user-visible gain; deferred.
Confidence rule = "never downgrade, user always wins", not timestamp-only or Bayesian. Last-write-wins by timestamp is what we're replacing (the original bug). A full probabilistic merge is over-engineered for a single-driver car. The two-line rule is testable and explainable. *Rejected:* both extremes.
People/Patterns built now, empty. *Rejected:* waiting for diarization/gpsd to exist first - that would force a subsystem rewrite later instead of a data-source swap. The scaffolds cost little and make the eventual integration mechanical.
episodes not re-touched. It is already covered by Session 7's persistent summaries; re-modelling it here would be churn. Four of six areas (vehicle, fuel, driver, episodes) now have typed/confidence backing; people and patterns are scaffolded pending hardware.
Icon shipped as the selected concept as-is; full-bleed flagged, not silently changed. The PNG has an inset rounded "card" with a dark margin, so under iOS's own corner mask the logo reads slightly padded rather than edge-to-edge. Rather than regenerate the chosen artwork unilaterally, this is flagged for Selim to decide. *Rejected:* silently altering an approved brand asset.
Did not trigger the production build/submit. It is billable, outward-facing, and gated on Apple 2FA. *Rejected:* kicking off eas build autonomously - it would hang on the credential prompt and burn a build credit.
eas build -p ios --profile production → `eas submit -p ios --profileproduction`** - the one manual, Apple-2FA step for TestFlight.
(services/spotify.py, api/routes/spotify.py); still needs a structured /spotify/now-playing endpoint and the two mobile surfaces. Deferred this session.
expo-doctor flags expo-av (unmaintained) and minorversion drift (async-storage, react-native, react-native-screens). Pre-existing; not touched here.
Credits. Idea & memory model: Mete Selçuk Şimşek. Engineering: Selim Fedakar. Tech lead: Atilla.