Commit Graph

797 Commits

Author SHA1 Message Date
null 7ab80013df fix(recovery): stop the two ways a returning user loses their history
Recovering on a new phone was walked end to end (a fixture was wiped, then
restored through the app's own flow). The Recovery screen itself is good — the
app detects the missing key and routes there by itself, and the copy is honest.
Everything around it had two holes, one of them destructive.

1. Sign-in could send a returning user into new-user profile setup (P2, data
   loss). OnboardingViewModel treated "couldn't read the profile" and "has no
   profile" as the same thing, so a slow read right after sign-in — a real race,
   hit live — routed them to CREATE_PROFILE, which asks "What should your
   partner call you?" over a `🔒 Couldn't unlock on this device` value. Anyone
   tapping through it re-encrypts and overwrites the name we had just failed to
   read, and never reaches Recovery. The repository already distinguishes the
   cases (missing doc = success(null), failed read throws), so only a successful
   read may now route to profile setup; a read that never succeeds retries and
   then goes Home, which is non-destructive and routes to Recovery on its own.

2. A correct phrase was reported as wrong. It is Argon2id key material, so it is
   byte-exact, and only .trim() was applied — while the field allowed the
   keyboard to capitalise the first word and autocorrect the wordlist. Every
   generated phrase is lowercase a-z single-spaced (all 248 WORDLIST entries
   checked), so folding typed input to that canonical form is lossless and
   cannot weaken the KDF: it only removes failures that were never about the
   phrase being wrong. "That phrase doesn't match" now means it actually
   doesn't. Also sets KeyboardCapitalization.None + autoCorrectEnabled = false.

3. The escape hatch was styled as a footnote. Most people arriving here are on a
   new phone and never saved the phrase, so "ask my partner" — not the field —
   is their real way through, yet it was a bare TextButton under the one control
   they can't use. It is now a full-width OutlinedButton with plainer copy
   ("I don't have the phrase — ask my partner"). It stays below the field rather
   than replacing it: someone who does have the phrase pasted from a message is
   unlocked instantly and offline, while this path waits on the partner.

Adds RecoveryPhraseNormalizationTest (7 cases): auto-capitalisation, shouting,
double/tab/newline spacing and chat-pasted text all fold to the canonical phrase,
while a genuinely wrong phrase still differs. Unit suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:25:20 -05:00
null 5151698d56 fix(avatar): harden the cropper's error handling; unify with the codebase
Checking error handling on the crop sheet surfaced three real problems, two of
which I had introduced.

1. Crash on a high-megapixel photo. The picked image was decoded at full size
   and handed to a hardware Canvas. An 8000x8000 photo decodes to 256MB, which
   Android allocates but the canvas refuses to draw ("trying to draw too large
   bitmap") — thrown inside Compose's draw phase, where the runCatching around
   the decode can't see it. Proven live: an 8000x8000 pick killed the app.
   Fixed by subsampling at decode (inSampleSize from a bounds-only pass) so the
   working bitmap is capped at ~2048px / ~16MB. Native heap on that pick went
   256MB(attempted) -> 46MB.

2. The subsample fix then broke decoding for EVERY image. decodeStream returns
   null by design in inJustDecodeBounds mode, and I had `?: return null` on it —
   so loadOriented bailed right after the bounds pass, for all inputs. It only
   looked like "the huge photo failed"; a normal photo would have failed too. I
   never re-tested a small image after the change. The new AvatarCropSheetTest
   caught it. The stream is now what's guarded; the real check is the header size.

3. Errors were swallowed and off-standard. runCatching{}.getOrNull() dropped the
   cause and a failure silently dismissed the sheet — indistinguishable from a
   save that did nothing. Now unified with the screen's own pattern
   (EditProfileViewModel.save): the sheet reports the Throwable up via a new
   onError, the VM records it through the injected CrashReporter and surfaces
   the message through uiState.error -> the existing snackbar. The crop also
   moved off the main thread (withContext(IO)); inline, `saving` flipped within
   one frame and never showed.

Adds AvatarCropSheetTest (androidTest, 4 tests, on-device BitmapFactory/Canvas):
subsample math for 8000+/oversized, oversized decode stays bounded, garbage
returns null instead of throwing, crop output is square and bounded. All green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:58:38 -05:00
null 7b8fa652f6 feat(avatar): frame a picked photo, and fall back to initials not a heart
Initials, shared. Home's partner bubble and the Settings "Connected with" row
are two views of one person and gave different answers when there was no photo —
Home showed initials, Settings showed a heart. Settings now shows the partner's
initials in a 48dp circle, so the photo and no-photo states share one
silhouette. The helper is lifted to ui/components/Initials.kt rather than copied;
an identical copy in two files is exactly how packArtworkRes drifted. The
unpaired state keeps the heart — there is nobody to take initials from yet.

Framing. Picking a photo now opens a crop sheet: pinch to zoom, drag to move,
inside the real avatar circle. The avatar was a blind ContentScale.Crop, so a
non-square or off-centre photo was centre-cropped and could lose the subject —
Ava's 640x480 test photo cropped straight past her face. The crop is applied at
pick time and handed back as a normal Uri, so setPhotoUri → upload is untouched.
EXIF orientation is honoured (a portrait selfie would otherwise crop sideways),
output is a 512px JPEG, and the source aspect is respected.

Two bugs found by driving it live, both of which looked fine in code:
- Panning did nothing at 1x. The clamp was viewport*(scale-1)/2, which is 0 at
  1x — correct only for a square source. A cover-fit 640x480 photo already
  overflows horizontally at 1x, so that pinned it dead centre and made framing
  impossible: the whole point of the sheet. Now clamped to the picture's actual
  overflow, computed from the source aspect.
- Panning then revealed empty space at the circle's edge. ContentScale.Crop had
  already discarded the overflow and returned a viewport-sized node, so
  graphicsLayer was sliding an already-cropped square around — the pixels being
  panned to had been thrown away first. The preview now draws the bitmap itself
  at cover*scale with the same maths as the crop, so preview == output.

Adds androidx.exifinterface. Verified live on 5554: sheet opens, pan moves the
picture (pixel-probed), no empty edge. 0 FATAL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:19:37 -05:00
null 76d8746a9b fix(settings): the paired "Connected with" card rendered muddy and unpolished
Two defects, both only in the paired state — the unpaired state was already
styled correctly, which is why this hid.

1. The card was see-through. `partnerCardColor` used primaryContainer at 52%
   alpha, and the page behind it is a gradient brush, so the gradient bled up
   through the card: it read muddy against the crisp profile card directly above
   (which is 96%), and the card's own content bounds showed as a paler,
   sharp-edged band inside it — measured, not guessed: a 24-unit jump
   (218,209,226 -> 242,232,251 -> 217,208,225) across the Row's 18dp content
   inset. Now 96% like its sibling; the same scanline is flat (240,229,253 ->
   241,231,255). Content colours are the container's own on* pair rather than
   the page's ink, so contrast is correct in both themes.

2. The heart was a bare, outsized glyph. ProfileAvatar's no-photo fallback is a
   40dp Icon with no container, so the paired row showed a naked heart unlike
   anything else on the page — while the *unpaired* branch right below it, and
   every settings row, use a 48dp tile + 24dp glyph. The avatar is now used only
   when a real partner photo exists; otherwise it falls back to that same tile.

Verified live both fixtures (dark 5554 / light 5556) with pixel probes before
and after. theme-scan REVIEW 25 -> 24 (one hardcoded-colour hit removed);
CRITICAL/MAJOR unchanged. Unit tests green, 0 FATAL.

Filed alongside this round's Pass C results (R32) in ClaudeReport/ClaudeQACoverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:29:05 -05:00
null eb254d9a9c docs(future): reconcile the QA backlog with what actually shipped
Three claims had gone stale and one was still true; verified each against the
tree rather than assuming.

Corrected:
- sexual_preferences non-binary configs are 110/150, not 48 — the pack was
  rewritten to the 150-cap since the item was filed.
- The androidTest item still described a single first-cut suite; there are now
  three (FirstRunRenderSmoke, HomeContentRenderSmoke, AssetDatabaseVerify).
- daily_fun_mc sex='neutral' re-verified as still true (all 511) on the rebuilt
  DB, so the item stays open on purpose.

Added a Content pipeline & app size section recording what shipped this cycle
and what is still open — none of it was tracked anywhere: build_db.py could not
produce a loadable database, the shipped bank was 2,292 questions out of sync
(wildcards and quality_time were entirely absent from the app), scale labels
were blank because the DB and the parser disagreed on case, and artwork went to
WebP (104M -> 28M; APK 141.9 -> 128.8 MB) after two tracked .bak databases were
found shipping inside assets/.

Still open, newly recorded: pack art is nodpi at 1024x512 (~3x the pixels the
~360dp card needs, and nodpi means no density split, so every device downloads
all 11.5 MB); and nothing in CI validates the seed JSON — every gate we have
runs only when a human remembers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:50:04 -05:00
null 7700c84e4d test(qa): add nav-scan.sh — prove navigation dead ends statically
QA had no net for navigation. Pass C already mandated "from every screen confirm
there's always a way out", and C-NAV-004 still shipped: the pack library drew no
app bar and no bottom bar, stranding the user on the system Back gesture. A human
walking ~50 routes misses one.

A dead end is statically provable, so it should not be hunted by eye. The shell
draws chrome from two hand-kept sets — topLevelRoutes (bottom bar) and
shellBackRoutes (app bar + back). A route in neither, whose screen also draws no
back of its own, has no exit by construction. nav-scan.sh cross-references both
sets against each route's screen composable and reports those as CRITICAL,
exiting 1 so it can gate. It follows the existing scanner family (theme-scan,
painter-xml-scan, wiring-scan) and is wired into Pass C as a pre-check, run
before the manual sweep like the theme scan.

Proven both ways rather than assumed: it flags QUESTION_PACKS when the fix is
reverted, and reports 0 with the fix in place. Scanning the whole route table
found no other dead ends. Excludes the entry flow (C-NAV-001: back out of Home
must not resurface onboarding) and two self-contained flows whose own CTAs are
the exit (PAIR_PROMPT, RECOVERY).

Also records the durable substance where it belongs:
- ERM landmine C-NAV-004 — the exact inverse of C-NAV-003 (wrongly *added* to
  shellBackRoutes = double app bar; wrongly *omitted* = no bar at all). The two
  failure modes point in opposite directions, so there is no safe default; the
  scanner is the guard.
- Future.md — the Tier 3 screenshot-diff idea is no longer hypothetical. Five
  visual defects surfaced in one day (star glyph fallback, "Chat Bubble Outline"
  chip leak, mid-word truncation, double back arrow, hard-edged art) and every
  one was caught only by a human reading a screenshot. They share a shape: the
  UI stays renderable and shows the wrong thing, so compile/unit/theme-scan all
  stay green. That is the class the tooling is structurally blind to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:46:24 -05:00
null 089437e415 fix(nav): give the question pack library a way back
The pack library was a navigation dead end: it is not a bottom-nav tab and it
draws no header of its own, and it was missing from shellBackRoutes — so the
shell rendered neither an app bar nor a bottom bar. Once there, the system Back
gesture was the only way off the screen.

It is reached by drilling in from four places (Home "All packs", the Play hub,
the question composer's empty state, and the weekly recap), so it needs the same
shell back affordance its own detail page (QUESTION_CATEGORY) already had.

Audited the whole route table for the same hole rather than patching just this
one. Every other non-tab route either sits in shellBackRoutes or draws its own
back; the two remaining are PAIR_PROMPT and RECOVERY, which are self-contained
entry flows with their own CTAs, so they are intentionally left alone.

Verified live on both fixtures: Home -> Question Packs -> a pack -> back ->
Question Packs -> back -> Home, in dark and light. Unit tests green, 0 FATAL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:31:13 -05:00
null 66d38251ea fix(packs): show pack name and description in full on library cards
The library cards clipped the two things a card exists to communicate: the name
at maxLines=1 ("Communicati…") and the description at maxLines=2 ("…about
pers…"). Both are now uncapped and cards size to their content, matching the
pack detail page.

Removing the caps alone was not enough. The question-count pill shared the title
row, so it reserved width for the row's full height and squeezed the text into a
narrow column — the name then broke mid-word ("Communicatio/n") and the
description wrapped to a ragged 8 lines. The pill moves down to join the access
pill, which gives the text the card's full width and matches how the detail page
already groups its pills.

Verified live on both fixtures (dark 5554 / light 5556): full names on one line,
full descriptions across the card, pills grouped below. Unit tests green,
0 FATAL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:24:26 -05:00
null 8aea4730cc perf(res): convert all artwork to WebP; unify + blend pack art
Converts every PNG in res to WebP (124 files, 104M -> 28M on disk, -73%).
Each file was encoded both lossless and lossy q95 and the smaller kept, but
lossy only where it measured visually lossless (PSNR >= 40 dB); dimensions and
alpha were verified per file before the PNG was removed, so 25 files that
compressed better losslessly stayed exact. No nine-patches exist to break, and
R.drawable refs are extension-agnostic (the only ".png" in code is a runtime
share-cache filename). Debug APK 141.9 -> 128.8 MB; pack art in the APK
26.4 -> 11.5 MB. The disk saving is larger than the APK saving because AAPT2
already crunched PNGs at build time.

Unifies pack artwork in a new PackArtwork.kt. packArtworkRes was duplicated
verbatim in the library and detail screens — which is exactly how the two
drifted before (one kept a grouped mapping that gave several packs the same
illustration). It now exists once, alongside the two art composables. This is
also where an imported pack's art would resolve.

Pack detail page fixes:
- Removed the second back arrow. The nav scaffold already supplies the
  "Question Pack" bar and its back affordance (AppRoute.kt), so the in-screen
  IconButton was a duplicate; onBack is now unused and gone.
- The description is shown in full. It was capped at maxLines=4 with an
  ellipsis, which truncated most packs mid-sentence — this is the page where
  someone decides whether to open the pack, so it should not be abridged.
- The hero art is blended instead of pasted on: it runs full-bleed (the list no
  longer pads horizontally; items pad themselves) and dissolves into the page.
  The dissolve is an alpha mask (BlendMode.DstIn), not a scrim in the
  background colour — the page behind is a gradient brush, so fading to any
  single colour left a visible pale band in light theme. Fading the image to
  transparent lets the real background through, correct in both themes.
  Library cards get a softer version of the same, into the card surface.

Verified live on both fixtures (dark 5554 / light 5556): single back arrow,
full description, art melting into the page with no hard edges, correct
per-theme art. Unit tests green, 0 FATAL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:52:14 -05:00
null 388b927437 feat(packs): add distinct pack artwork 2026-07-14 20:18:56 -05:00
null 1ac724bcde fix(content): give packs a pack_id, and stop icon_name leaking into the UI
Populates question.pack_id (build_db.py). The column was plumbed end to end
(question.pack_id -> QuestionEntity.packId -> Question.packId) but was NULL for
all 3811 rows, so content had no pack identity. Bundled packs now carry one —
the logical id a pack declares in metadata (daily ships as category
daily_fun_mc but is logically daily_single_choice_weekly_v1), else its category
id. This is the hook a purchased/imported pack needs: it becomes just another
pack_id in the same table rather than a special case. No schema or behaviour
change; identity hash unchanged.

Fixes two regressions the rebuilt db exposed, both latent for the same reason:
every category previously carried the placeholder icon_name "question", which
masked them. Packs now declare real Material icon names (shield, forum, paid).

1. Category glyphs. categoryGlyphStyle keyed on `iconName ?: categoryId`, so
   icon_name won. Material names are a different vocabulary from this file's
   keys, so 22 of 23 categories fell through to the default star. The curated
   per-category glyph now wins, with icon_name as the fallback — which is also
   what gives an imported pack a real glyph, since its category_id will not be
   listed here but its declared icon_name resolves. Added the Material aliases
   and the two unmapped categories (quality_time, daily_fun_mc).
2. Pack library chips. metadataLabels() rendered the raw icon_name as a
   user-facing tag, producing chips like "Chat Bubble Outline". It was only ever
   invisible because the list filtered the single value "Question". icon_name is
   a rendering detail, not a topic, so the chip is gone; access remains.

Verified live on both fixtures (dark 5554 / light 5556): distinct correct glyphs
(Boundaries warning, Communication chat, Rebuilding Trust shield, Sex & Desire
heart-outline), no leaked chips, and the new Quality Time pack browsable with
150 questions and a calendar glyph. Unit tests green, 0 FATAL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:10:37 -05:00
null 8e6dabd4a4 feat(seed): rebuild asset db from JSON; verify Room loads it on-device
Regenerates app/src/main/assets/database/app.db with the fixed build_db.py, so
the app finally ships the current question catalog. The db had drifted far from
the JSON source of truth: 6103 -> 3811 questions (the intentional 150-cap trim),
+150 quality_time (a category the app could not show at all), and daily 500 ->
511 — the 11 wildcards, whose feature (DailyModeResolver) was built but had zero
content in the db, so wildcard days were dark. Identity hash is unchanged
(7e7d78fc...), schema untouched, so no migration is involved.

Scale labels now actually render. The db stored snake_case answer_config while
the app's only parser (QuestionMapper.parseAnswerConfig) reads camelCase, so
optString("minLabel","") resolved to "" and the scale UI fell back to bare
numbers. The rebuild emits the shape the parser reads.

Adds AssetDatabaseVerifyTest (androidTest): Room opens the asset lazily, so an
app that launches proves nothing about it — the bundled db is only validated on
first DB touch, and a bad one crashes every user on a fresh install. The test
forces a real open via openHelper.readableDatabase (triggering the copy +
identity/schema check) and asserts content, no orphan category_ids, integer
depth, and the camelCase scale contract. Verified 4/4 green on a clean install
on throwaway emulator 5558; the 5554/5556 fixtures were never touched.

Also stops shipping ~6MB of dead weight: app.db.bak_q4 and app.db.bak_q5 were
tracked inside assets/, and everything under assets/ is packaged into the APK.
build_db.py was making it worse by writing its backup next to the db; backups
now go to build/db-backups/ (gitignored, outside the packaged tree) and the
stale ones are removed. APK now contains only assets/database/app.db.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:49:20 -05:00
null eaedeb398c fix(seed): make build_db.py produce a Room-loadable asset database
build_db.py could not produce a shippable app.db and would have crashed the
app on first launch. It created only 2 tables (Question, QuestionCategory)
while AppDatabase declares 4 entities, and it never wrote room_master_table.
Since createFromAsset runs with no migrations and no destructive fallback,
Room validates strictly against identity hash 7e7d78fc... — so the output was
guaranteed to fail. Hence the standing "never run build_db.py" rule; the tool
had drifted from the schema it feeds.

Schema is now taken from Room's own exported schema
(app/schemas/app.closer.data.local.AppDatabase/1.json, via room.schemaLocation)
rather than hand-written DDL: all 4 entity tables + indices + the identity row
are created from it, so the script follows future schema changes automatically.
Verified the output's PRAGMA table_info/index_list is identical to the shipped
known-good db for all 5 tables, with a matching identity hash.

Also fixes the answer_config contract. The app's only parser
(QuestionMapper.parseAnswerConfig) reads camelCase keys, but the shipped db
stores snake_case and the old script emitted a third shape again — so scale
labels silently resolved to "" (the UI then shows bare numbers) and written
max_length was ignored. The authoring JSON stays snake_case per
QUESTION_SCHEMA.md; translating to the parser's shape is now this script's job.

Content problems are now hard failures instead of silent corruption. Previously
a JSON parse error or missing category made the script skip/mislabel a whole
pack ("unknown"). Now rejected with a precise message: parse errors, missing
category object (patch manifest under a production filename), zero questions,
missing id, duplicate id/text, invalid type/access, category_id mismatch,
non-integer depth, choice without options, this_or_that without exactly 2.
All 12 guards verified against mutated fixtures; a valid pack still builds.

Adds --check (validate only) and --out (build elsewhere). Builds are staged to
a temp file, verified, and only then moved into place, backing up the existing
asset db first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:37:59 -05:00
null ed1d9eafe4 review 2026-07-14 18:22:45 -05:00
null 2f6035d983 review 2026-07-14 12:13:07 -05:00
null 3422843a68 review 2026-07-12 19:55:14 -05:00
null fb9a876b98 docs(seed): add anti-AI-voice + humor-craft standard to question guides
The content agent (ChatGPT) authors packs from these guides, and the repo has
documented LLM over-compliance failures: snack overload, 416/499 same-mechanic
six-question blocks, exactly-6-of-each mechanic counts. The guides banned
therapy voice but never the machine-voice failure modes.

- QUESTION_CONTENT_GUIDE.md: new section 8A "Write Like a Human" — AI-tell
  phrase ban, no-exclamation/no-emoji/sentence-case rules, humor craft (joke
  lives in the detail not the adjective; one absurd detail max; adult-deadpan
  mock-formality register; punch at the situation never the partner; snort
  test at ~1-in-4), anti-uniformity guards (guide examples are categories not
  vocabulary; no template blocks in any 10 consecutive; uneven distribution
  is correct), texting test. Also retitled section 18 agent-neutral.
- DAILY_SINGLE_CHOICE_WEEKDAY_SYSTEM.md (v8→v9): "Daily Voice: Human and
  Funny" subsection — weekday theme is the angle not a template stem;
  wildcard absurdity rules; points at 8A as authority.
- QUESTION_QUALITY_CHECKLIST.md (v10→v11): machine-voice Automatic Rejects,
  implausibly-even mechanic-count repetition check, and 5 new marking reasons
  (ai_voice, labeled_joke, uniform_option_grammar, guide_example_parrot,
  brand_caption_voice) so patch discipline can target these failures.

Additive only: no schema/importer-contract, count, or patch-policy changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:14:34 -05:00
null d7d97cc8a8 review 2026-07-12 16:07:09 -05:00
null 75a644e1ba review 2026-07-12 16:02:05 -05:00
null 157f080ee6 review 2026-07-12 12:55:14 -05:00
null 89138874e7 review 2026-07-12 12:08:42 -05:00
null 25d3441ea9 review 2026-07-12 04:47:48 -05:00
null 5643a0945e docs(future): file the codebase-wide ImmutableList Compose-perf convention (Home refactor follow-up)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:55:26 -05:00
null 757d4efdda test(home): robust assertion in render smoke's unanswered-state case
The unanswered case asserted a specific CTA ('Answer privately') that the preview's
demo state doesn't surface in the test viewport. Assert the stable header instead —
still exercises the UNANSWERED render path (setContent), just without a brittle text
match. Verified 3/3 green on a throwaway emulator (fixtures untouched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:54:17 -05:00
null ed0aa4d3b3 fix(test): HomeContentRenderSmokeTest nested-comment (components/* opened a block comment)
'ui/home/components/*' in the KDoc contains a /* sequence, which Kotlin treats as
a NESTED block-comment opener (Kotlin nests block comments) -> unclosed comment.
Reworded. androidTest now compiles. (App was never affected.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:49:06 -05:00
null 1eacf36d81 fix(test): HomeContentRenderSmokeTest unclosed-comment (emoji in KDoc broke Kotlin lexer)
The prior commit's androidTest didn't compile — a stop-sign emoji in the KDoc
tripped the Kotlin lexer ('Unclosed comment'). Replaced with plain text. App was
never affected (androidTest isn't in the APK); this restores connectedAndroidTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:47:59 -05:00
null f7eb03417b test(home): HomeActionMapperTest (JVM) + HomeContentRenderSmokeTest (instrumented)
Part 3 of the Home refactor. Adds the durable coverage the plan called for:
- HomeActionMapperTest: 12 JVM cases over the lifted pure mapper (refresh states,
  withHomeActions pairing/daily/loading/error paths, toHomeLabel mc-drop, secondary
  cap of 3, C-HOME-001 primary/pending dedup) — locks the extraction as faithful.
- HomeContentRenderSmokeTest: instrumented render net for Home (via the VM-free
  PairedHomePreviewScreen), light + dark — catches 'composes fine, crashes on
  first paint'. Runs on a THROWAWAY only (uninstalls app-under-test).

JVM test green; androidTest compiles. Fills the 'Home has no UI test' gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:46:16 -05:00
null 9e81c15350 perf(home): @Immutable on HomeAction/PendingActionCard/HomeCategorySummary
Part 2b (Compose stack advantage). Marks the three verified-deeply-immutable
card models @Immutable so their single-instance params get structural-equality
skipping (upgrade over the K2 reference-equality strong-skipping default).
HomeAnswerStats/HomeUiState left unannotated (LocalAnswer/Question/Set transitive
types not fully audited — a false @Immutable promise would cause stale UI).
Rendered output identical; only recomposition frequency drops. compile green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:42:04 -05:00
null 7be9a1bf15 refactor(home): lift pure action mapper -> HomeActionMapper.kt (VM 867->562)
Part 2b. Moves the ~305 lines of pure HomeUiState action-derivation extensions
(refreshDailyQuestionState/withHomeActions internal; toHomeAction/
buildDailyQuestionAction/buildPendingActions/has*/toHomeLabel private) out of
HomeViewModel verbatim (de-indented, byte-identical bodies). Verified pure — no
this@HomeViewModel/repo refs. VM call-sites unchanged (same-package extensions).
compile + full unit suite green. HomeViewModel now 562 lines (from 1030).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:40:31 -05:00
null 227d141221 refactor(home): extract data models -> HomeModels.kt (VM 1030->867)
Part 2a. Moves the Home data classes/enums + computeDailyQuestionState (kept
@VisibleForTesting internal) + gameRouteFor (widened private->internal, its only
caller loadHome is same-package) out of HomeViewModel into HomeModels.kt.
Same package -> no consumer import churn (AppNavigation, DailyQuestionStateTest
unchanged). Behavior-identical. compile + ui.home unit tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:38:29 -05:00
null 415f8d6866 refactor(home): extract StreakMilestoneDialog + drop dead MomentCueCard
Step 9/9 of the UI split. StreakMilestoneDialog -> components/ (kept internal;
ArtPreviewScreen call updated to the new package). MomentCueCard was dead (no
callers) — removed. Orphaned imports cleaned. compileDebugKotlin green.
HomeScreen.kt now ~620 lines (from 1921).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:34:37 -05:00
null 5e266a95fa merge(home): HomeScreen UI decomposition (8 cluster files) into dev
Behavior-preserving split of HomeScreen.kt (1921 -> 712 lines) into
ui/home/components/: HomeActionStyle, HomeStateCards, HomeCategoryPreview,
HomePendingCards, HomeActionFeed, HomeStatusStrip, HomePrimaryCards,
HomeHeaderSection. Each step compile-gated. ViewModel extraction + tests still
pending (tracked separately).
2026-07-11 19:59:45 -05:00
null 94360a5438 refactor(home): extract header + partner sheet -> components/HomeHeaderSection.kt
Step 8/9. Verbatim move (HomeHeader + PartnerQuickActionsSheet public, rest private;
@OptIn(ExperimentalMaterial3Api) + .semantics a11y + full state param preserved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:43:22 -05:00
null c13dbc5bfd refactor(home): extract primary/activation cards -> components/HomePrimaryCards.kt
Step 7/9. Verbatim move (activation + primary hero public, benefit-pill private).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:40:08 -05:00
null d324efe182 refactor(home): extract status strip -> components/HomeStatusStrip.kt
Step 6/9. Verbatim move (strip public, chip private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:32:54 -05:00
null 48197ffd1e refactor(home): extract action feed -> components/HomeActionFeed.kt
Step 5/9. Verbatim move (section public, card private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:31:42 -05:00
null 88460922c3 refactor(home): extract pending cards -> components/HomePendingCards.kt
Step 4/9. Verbatim move (section public, card-view private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:30:25 -05:00
null 07c810c9a1 refactor(home): extract category preview -> components/HomeCategoryPreview.kt
Step 3/9. Verbatim move (grid public, mini-card private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:29:06 -05:00
null 18ad31506e refactor(home): extract loading/error/empty -> components/HomeStateCards.kt
Step 2/9. Verbatim move, public entries. compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:27:52 -05:00
null 5f39194c00 refactor(home): extract shared card kit -> components/HomeActionStyle.kt
Step 1/9 of the HomeScreen decomposition. Moves the 6 shared style decls
(homeActionGlyph, HomeGlyphIcon, homePrimaryArt, HomeActionColors,
HomeActionTone.actionColors, HomePill) verbatim into ui/home/components/, made
public per the components/ visibility idiom. No behavior change. compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:26:08 -05:00
null 047185134e review 2026-07-11 19:17:51 -05:00
null eb70177178 revoew 2026-07-11 19:15:33 -05:00
null a7f252e656 review 2026-07-11 18:49:40 -05:00
null a63bf350b1 fix(crash): LocalDate.ofInstant crashes on API<34 + add Lint/R8 CI gates
Adding Android Lint to CI immediately caught two real crash bugs invisible to
all emulator QA (the fixture emulators run API 34+):

- SettingsViewModel + YourProgressViewModel called LocalDate.ofInstant, which
  was only added in API 34 — but minSdk is 26. On every device running Android
  8-13 (the bulk of the install base) these throw NoSuchMethodError and crash
  the Settings and Your Progress screens. Fixed with the API-26-safe equivalent
  Instant.atZone(zone).toLocalDate() (same result).

The other two Lint errors were false positives (ProduceStateDoesNotAssignValue
on two EncryptedChatImage composables that DO assign value inside the producer —
the check misfires on a suspend/?.let RHS) — explicitly @Suppress'd with a note,
so Lint reaches 0 errors legitimately rather than via a blanket baseline.

CI (android-ci.yml) gains two jobs:
- android-lint: ./gradlew :app:lintDebug (fails on error-severity; 113 existing
  warnings are non-fatal and left for a separate burndown).
- release-build: first-ever R8 gate — builds :app:bundleRelease with a throwaway
  keystore + dummy RC_API_KEY (satisfies the release guards; AAB not distributed),
  so the minify/shrink/sign toolchain can never silently rot. Verified locally:
  bundleRelease SUCCEEDS today (95MB AAB). A green build proves the toolchain,
  not runtime survival of reflectively-loaded classes (Tink) — that stays a
  release-APK QA item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:41:04 -05:00
null 53d18beb03 feat(seed): sex_and_desire light-depth batch 6 of 6 (6 written, 338->344) - hits 40.1% light 2026-07-11 18:32:47 -05:00
null 5af109e119 feat(seed): sex_and_desire light-depth batch 5 of 6 (8 single + 4 multi + 4 tot + 4 scale, 318->338) 2026-07-11 18:32:15 -05:00
null a362742494 feat(seed): sex_and_desire light-depth batch 4 of 6 (12 written + 4 single + 4 tot, 298->318) 2026-07-11 18:31:30 -05:00
null 57b0f02763 feat(seed): sex_and_desire light-depth batch 3 of 6 (12 written + 4 single_choice, 282->298) 2026-07-11 18:30:51 -05:00
null b3165645e7 chore(repo): track the repaired pre-push hook + installer; backlog entries (Batch 2)
- scripts/git-hooks/pre-push: the fixed hook (dirty-tree guards kept; the
  self-defeating 'unpushed commits' blocker replaced with a behind-remote
  fail-fast + an informational listing of what a push will publish). Hooks
  don't sync on clone, so scripts/install-git-hooks.sh installs them; verified
  idempotent against the live .git/hooks copy.
- Future.md: scheduledOutcomesReminder .limit(200) no-pagination scaling cap
  recorded with the fix pattern (assignDailyQuestion's page loop); npm-audit
  entry re-verified 2026-07-11 (still 9 moderate, transitive; no lockfile churn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:30:27 -05:00
null 25c463ee2a feat(seed): sex_and_desire light-depth batch 2 of 6 (16 written, 266->282) 2026-07-11 18:30:21 -05:00