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>
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>
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>
Self-review of the wildcard authoring spec caught two real issues:
1. Count contradiction: the new Wildcard section said '+12 free' while the pack's
authoritative counts (QUESTION_SCHEMA.md daily table + metadata, and the guide's
Required Counts + Final Production Gate 'count validation') still said 500/75. A
content agent running count validation would fail, or worse delete weekday
questions to hit 500. Reconciled all four locations to the weekday pack (frozen
500) + wildcard add-on -> 511 total / 86 free / 425 premium, and folded the
wildcard set into the Final Production Gate review sampling.
2. Rotation math: the picker indexes epochDay % poolSize and wildcard days fall ~10
apart, so a pool size sharing a factor with 10 (12 is even) only surfaces a
fraction of the questions per year. Pool size must be coprime with 10 -> 11, not
12. Documented as a hard constraint so it isn't 'rounded to 10' later.
Also hardened the section: net-new (not a patch), answer_config option mirror,
unique snake_case option ids, and a QUESTION_SCHEMA.md cross-reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client's DailyModeResolver promotes ~10% of days (dayOfYear % 10 == 3) to a
'Wildcard' theme, but there were zero wildcard questions and the server never
assigned one — so those days showed the Wildcard banner over a normal weekday
question. This adds the server half:
- pickDailyQuestionId now detects wildcard days (new pure, tested dayOfYearUtc /
isWildcardDay helpers mirroring the client cadence) and prefers the mode_wildcard
pool. Until that content is seeded it falls back to the day's WEEKDAY mode (not
the whole pool), so it's a safe no-op pre-content. +3 unit tests (fn 80 -> 83).
- Authoring spec for the missing content added to
seed/questions/DAILY_SINGLE_CHOICE_WEEKDAY_SYSTEM.md (## Wildcard Mode): 12 free
single_choice, day-agnostic voice, REQUIRED mode_wildcard tag, id scheme, schema,
and the post-authoring rollout (asset-db data-only insert + Firestore pointer seed
+ deploy).
Content authoring handed to another agent per the guide. Takes effect after the
wildcard rows land in app.db + Firestore and assignDailyQuestion is deployed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>