chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
# Design
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Status: Current
|
|
|
|
|
Owner: _null
|
|
|
|
|
Last reviewed: 2026-08-18
|
feat: the designed Settings screen, structured as §36 specifies
closes #33
Settings is now a root with §36's sections rather than one screen of notification
toggles that called itself a working surface. Reminders move underneath it as a
child destination; the banner is gone because the claim it made stopped being
true, not because the screen changed.
A ROW FOR AN UNBUILT FEATURE IS ABSENT, NOT DISABLED
§36 lists six sections and this shows the two that have something behind them.
A greyed-out "Export My Data" is a promise with no delivery date, and a disabled
"Delete My Data" tells a user that control over their own health data exists and
that they may not have it. In a product whose whole argument is that the data is
theirs, that is a bad first thing to say. Privacy & Security arrives with #34 to
#37, Appearance with Batch 08, Premium with Batch 07.
ONE COPY OF THE FERTILITY DISCLAIMER
About needs it, and it was already a literal in TodayScreen and CalendarScreen —
a third copy would have made two versions of a safety promise inevitable. Now a
string resource all three read. Notifications still deliberately omit it: a long
sentence about fertility on a lock screen is the leak the discreet copy exists
to prevent.
The version is read from PackageManager rather than BuildConfig, so it reports
what is actually installed — the number a person reads out when something is
wrong.
A DOC TRIGGER THAT COULD NOT FIRE
docs/design/README.md's trigger is "any new user-facing screen or state" while
its Governs listed only docs/design/** and core/designsystem/**, so adding a
screen fired nothing. Same class of defect as the architecture doc's, found the
same way — by adding the thing the trigger is about and watching it not fire.
Widened, and verified by running doc-triggers.py against the new file.
Driven on PeriodMinSdk26: the root renders, rows expand in place, reminders open
as a child, and back returns to the root rather than exiting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:44:15 -05:00
|
|
|
Governs: docs/design/**, core/designsystem/**, app/src/main/kotlin/**,
|
|
|
|
|
app/src/main/res/values/** — the design tokens, every user-facing
|
|
|
|
|
screen, and the product's tone and interface copy
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
Review trigger: Any new user-facing screen or state; any change to the colour or
|
|
|
|
|
type tokens; any change to notification copy or to a privacy or
|
|
|
|
|
fertility disclaimer
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Where the detail is
|
|
|
|
|
|
|
|
|
|
The screen-by-screen specification and the actual words are
|
|
|
|
|
[`../planning/PRODUCT_PLAN.md`](../planning/PRODUCT_PLAN.md), and they are not
|
|
|
|
|
repeated here — a second copy of interface copy is how two versions of a
|
|
|
|
|
disclaimer come to exist.
|
|
|
|
|
|
|
|
|
|
| Subject | Section it owns |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| Onboarding, seven screens, with copy | §19, §56 |
|
|
|
|
|
| Navigation — four tabs | §20 |
|
|
|
|
|
| Today screen and its six dynamic states | §21, §22 |
|
|
|
|
|
| Period logging, period end, spotting | §23, §24, §25 |
|
|
|
|
|
| Calendar states and markers | §26 |
|
|
|
|
|
| Insights | §27 |
|
|
|
|
|
| Notification modes, types and flow | §28, §29, §30 |
|
|
|
|
|
| Incognito launcher | §32 |
|
docs: correct 57 claims the code and tracker disagreed with
Every document in the tree audited against the source, the tracker and git
history, each finding then given to a second reader tasked with refuting it.
74 raised, 12 refuted, 57 applied. No code changed.
THE README DESCRIBED A SKELETON
Its Status table — the one place a claim about what is built is allowed to live
— still read "there is no usable app yet", with Not built against Room, the four
core screens, fertility and notifications, and No round run against QA. Five
batches had shipped and three QA rounds had run.
TWO DOCUMENTS WERE SILENTLY NEVER FIRING
architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle
module graph", "the design tokens in core/designsystem"). Neither contains a
path token, so doc-triggers.py reduced them to globs matching nothing, and one
real glob apiece made them look path-governing rather than subject-governing —
the state the script's own header calls invisible. Editing a Room entity never
fired the document owning the migration table. Both now fire, proved by running
the script.
SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS
App lock listed among what works offline; biometric/PIN gating described as
protecting app launch; the incognito launcher as existing; Play Billing in the
third parties table without the "not yet integrated" marker its neighbours
carry. All are Batch 06/07 work.
The advertising boundary was overstated in SECURITY.md and the README alike:
both said the ads module declares no dependency and a guard proves it. There is
no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than
the sentence it replaced and matches nothing until Batch 07, which is why the
guard is proved by injection rather than trusted.
SMALLER, EACH A REAL TRAP
WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template
path absent here — missed by doc-claims.sh, which reads backticked prose and not
fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three
rounds because ClaudeQAPlan.md's after-a-round list never named that section;
the playbook is fixed first. The instrumented-test count was eight in three
places and is four. HISTORY.md said the repository had no code and that nothing
had been tried and dropped, when three approaches had.
DELIBERATELY UNCHANGED
ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
| Look and feel, visual direction, typography, motion | §37, §38, §40, §41 |
|
|
|
|
|
| Colour | §39 — **superseded by [`BRAND_GUIDE.md`](BRAND_GUIDE.md)**; see [Colour](#colour) below |
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
| Artwork | §42 |
|
|
|
|
|
| Accessibility | §43 |
|
|
|
|
|
|
|
|
|
|
This document holds what governs those: the tone, and the rules that decide an
|
|
|
|
|
argument the specification did not anticipate.
|
|
|
|
|
|
|
|
|
|
## Tone
|
|
|
|
|
|
|
|
|
|
Calm, private, intelligent, adult. The app is a good utility with warmth — it is
|
|
|
|
|
not clinical, not childish, not gamified, and not stereotypically feminine as an
|
|
|
|
|
identity. It never celebrates a period and never alarms about one.
|
|
|
|
|
|
|
|
|
|
One paragraph, and it settles most small arguments: **the app speaks like
|
|
|
|
|
someone competent who is not making a fuss.** "Your period is late!" is out.
|
|
|
|
|
"Not yet?" with an updated forecast is in.
|
|
|
|
|
|
|
|
|
|
## Four rules that decide the arguments
|
|
|
|
|
|
|
|
|
|
**1. The number is the hero.** The forecast dominates the Today screen —
|
|
|
|
|
[§38](../planning/PRODUCT_PLAN.md). Anything competing with it for attention is
|
|
|
|
|
wrong, including anything of ours.
|
|
|
|
|
|
|
|
|
|
**2. Nothing asserts certainty the model does not have.** A window and a
|
|
|
|
|
confidence label, never a bare exact date presented as fact. "Estimated
|
docs: correct 57 claims the code and tracker disagreed with
Every document in the tree audited against the source, the tracker and git
history, each finding then given to a second reader tasked with refuting it.
74 raised, 12 refuted, 57 applied. No code changed.
THE README DESCRIBED A SKELETON
Its Status table — the one place a claim about what is built is allowed to live
— still read "there is no usable app yet", with Not built against Room, the four
core screens, fertility and notifications, and No round run against QA. Five
batches had shipped and three QA rounds had run.
TWO DOCUMENTS WERE SILENTLY NEVER FIRING
architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle
module graph", "the design tokens in core/designsystem"). Neither contains a
path token, so doc-triggers.py reduced them to globs matching nothing, and one
real glob apiece made them look path-governing rather than subject-governing —
the state the script's own header calls invisible. Editing a Room entity never
fired the document owning the migration table. Both now fire, proved by running
the script.
SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS
App lock listed among what works offline; biometric/PIN gating described as
protecting app launch; the incognito launcher as existing; Play Billing in the
third parties table without the "not yet integrated" marker its neighbours
carry. All are Batch 06/07 work.
The advertising boundary was overstated in SECURITY.md and the README alike:
both said the ads module declares no dependency and a guard proves it. There is
no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than
the sentence it replaced and matches nothing until Batch 07, which is why the
guard is proved by injection rather than trusted.
SMALLER, EACH A REAL TRAP
WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template
path absent here — missed by doc-claims.sh, which reads backticked prose and not
fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three
rounds because ClaudeQAPlan.md's after-a-round list never named that section;
the playbook is fixed first. The instrumented-test count was eight in three
places and is four. HISTORY.md said the repository had no code and that nothing
had been tried and dropped, when three approaches had.
DELIBERATELY UNCHANGED
ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
ovulation", never "you are ovulating today". Every **screen** that shows a
|
|
|
|
|
fertility estimate carries the not-contraception line, which is where
|
|
|
|
|
[§18](../planning/PRODUCT_PLAN.md) scopes it — "near fertility features and in
|
|
|
|
|
About". Notification copy is the deliberate exception: it says "Estimated
|
|
|
|
|
fertile window" and stops, because a disclaimer is a long sentence about
|
|
|
|
|
fertility on a lock screen.
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
|
|
|
|
|
**3. State is never colour alone.** Confirmed period is a solid fill, predicted
|
|
|
|
|
is dotted or outlined, fertile window is a ring, ovulation is its own small
|
|
|
|
|
marker — distinguishable in greyscale, because that is also what makes them
|
|
|
|
|
distinguishable to a colourblind user and to a screenshot in a bug report.
|
|
|
|
|
Predicted and confirmed days must never look identical.
|
|
|
|
|
|
docs: correct 57 claims the code and tracker disagreed with
Every document in the tree audited against the source, the tracker and git
history, each finding then given to a second reader tasked with refuting it.
74 raised, 12 refuted, 57 applied. No code changed.
THE README DESCRIBED A SKELETON
Its Status table — the one place a claim about what is built is allowed to live
— still read "there is no usable app yet", with Not built against Room, the four
core screens, fertility and notifications, and No round run against QA. Five
batches had shipped and three QA rounds had run.
TWO DOCUMENTS WERE SILENTLY NEVER FIRING
architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle
module graph", "the design tokens in core/designsystem"). Neither contains a
path token, so doc-triggers.py reduced them to globs matching nothing, and one
real glob apiece made them look path-governing rather than subject-governing —
the state the script's own header calls invisible. Editing a Room entity never
fired the document owning the migration table. Both now fire, proved by running
the script.
SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS
App lock listed among what works offline; biometric/PIN gating described as
protecting app launch; the incognito launcher as existing; Play Billing in the
third parties table without the "not yet integrated" marker its neighbours
carry. All are Batch 06/07 work.
The advertising boundary was overstated in SECURITY.md and the README alike:
both said the ads module declares no dependency and a guard proves it. There is
no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than
the sentence it replaced and matches nothing until Batch 07, which is why the
guard is proved by injection rather than trusted.
SMALLER, EACH A REAL TRAP
WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template
path absent here — missed by doc-claims.sh, which reads backticked prose and not
fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three
rounds because ClaudeQAPlan.md's after-a-round list never named that section;
the playbook is fixed first. The instrumented-test count was eight in three
places and is four. HISTORY.md said the repository had no code and that nothing
had been tried and dropped, when three approaches had.
DELIBERATELY UNCHANGED
ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
**4. Ads never touch a health action.** No banner in onboarding, in the
|
|
|
|
|
period-start confirmation, in the period-end confirmation, or between steps of a
|
|
|
|
|
health workflow — and never an interstitial after logging
|
|
|
|
|
([§33](../planning/PRODUCT_PLAN.md)).
|
|
|
|
|
|
feat: the Today screen and its six states, with the number as the hero
§21 and §22, each state its own screen rather than a variant of one. Which one
applies is decided by CycleStatusRules in a pure module with twelve tests on its
boundaries — and the boundaries are the point, because they are the days this
screen is most read: the day a period is due, the day after one ends, the day a
forecast slips.
The number dominates (§38): displayLarge at 72sp, in the primary colour, with
the unit as a separate quiet line so "4" reads instantly and "DAYS" is there if
you look. Its content description carries the whole sentence, so TalkBack says
"Period likely in: 4 days" rather than reading a bare numeral.
The state this screen exists to get right is the last one. Past the forecast the
app NEVER says late — late implies a schedule the user failed to keep, and the
truth is that an estimate was imprecise. It shows what it originally said, what
it says now, and asks.
A UX DEFECT FOUND BY DRIVING IT
Tapping "Period ended" changed nothing on screen. The logic was right — a period
that ends today still includes today, so the state does not change — but the
button looked broken, which is worse than being broken somewhere visible.
DuringPeriod now carries the end date, so the screen shows "Ended 18 August" and
offers only the useful action (undo) rather than a button that visibly does
nothing. §24's "Updated ✓" acknowledgement is there too. No test would have
caught this; it needed somebody to tap the button and look.
The banner slot is reserved and empty. §48 wants no layout jump when an ad loads
and a graceful gap when one fails, and both are properties of the space existing
whether or not it is filled — reserving it in Batch 07 instead means shipping
the jump first. Deliberately not a "your ad here" box, which would be a
placeholder for the thing a user pays to remove.
Fertility lines are absent rather than faked: §22 shows them and Batch 04
estimates them, and a placeholder number there would be inventing a fertility
estimate, which is the one thing this screen must not do.
Preview pairs for every state, light and dark. 129 tests, all passing.
closes #17
2026-08-18 03:50:28 -05:00
|
|
|
**The banner space is reserved before there is an ad to put in it.** §48 wants no
|
|
|
|
|
layout jump when one loads and a graceful gap when one fails, and both are
|
|
|
|
|
properties of the space existing whether or not it is filled. Retrofitting the
|
|
|
|
|
reservation in Batch 07 means shipping the jump first and finding it in a QA
|
|
|
|
|
round. It is empty and invisible now — deliberately not a "your ad here" box,
|
|
|
|
|
which would be a placeholder for the thing a user pays to remove.
|
|
|
|
|
|
feat: the real brand — Privacy:Period Tracker
The project owner supplied artwork and a full visual brand guide mid-session,
which answers #8: the three marks an agent must not fake now exist because a
person made them.
docs/data/img now holds icon.webp (512x512, 11 KB), logo.webp (1024 long edge,
28 KB) and banner.webp (2176x725, 20 KB) — all webp by magic bytes, all far
under the 512 KB ceiling. Produced from the supplied art rather than invented.
Two things had to be handled rather than converted blindly. The sources render
their rounded corners against black, so a naive crop carried black wedges into
the banner; the corners are flood-filled to the artwork's own ground first. And
the lockup is square, so a 3:1 crop would have cut the wordmark in half — the
banner is the whole lockup on a matching ground, which has no seam to see
because the artwork's edges ARE that ground.
The source PNGs and the guide moved to docs/design/: docs/data/img is for the
three marks the Command Center renders, and its README is explicit that
reference art and exported sources do not belong there.
THE PALETTE, AND ONE DELIBERATE CONFLICT
core/designsystem now carries the guide's colours — deep plum, rich purple,
period rose, lavender mist, and an eggplant dark theme that is never flat black.
§39 of the specification asks for a muted teal or sage fertile window, to hold
it apart from the period colour. The guide is plum and rose throughout and gives
fertility its own pink. The guide wins: it is newer, it is the owner's, and a
teal accent inside this identity would look borrowed from another app.
What §39 was protecting is kept by other means, and this is why the conflict is
safe to resolve that way: the calendar's states differ in SHAPE. A solid disc, a
dotted ring, a continuous ring and a star stay tellable apart in greyscale and
to a colourblind user, which is what §43 actually requires. The departure is
written down in docs/design/README.md rather than left to be discovered.
The launcher icon keeps its vector for now, in brand colours. The supplied
emblem has content close to its edges and an adaptive icon masks about a quarter
of the canvas away, so dropping it in unmodified would crop the shield. Fitting
it to the safe zone is Batch 08's final-artwork work.
closes #8
2026-08-18 15:31:52 -05:00
|
|
|
## Colour
|
|
|
|
|
|
|
|
|
|
The palette is [`BRAND_GUIDE.md`](BRAND_GUIDE.md), supplied by the project owner
|
|
|
|
|
on 2026-08-18. It **supersedes** the sketch in
|
|
|
|
|
[`../planning/PRODUCT_PLAN.md` §39](../planning/PRODUCT_PLAN.md) where the two
|
|
|
|
|
differ, and one difference is worth stating rather than leaving to be discovered:
|
|
|
|
|
|
|
|
|
|
> **§39 asks for a muted teal or sage fertile window. The brand guide is plum
|
|
|
|
|
> and rose throughout, and gives fertility its own pink (`#E891AE`).**
|
|
|
|
|
|
|
|
|
|
The guide wins — it is newer, it is the owner's, and a teal accent inside this
|
|
|
|
|
identity would look borrowed from another app. What §39 was protecting is kept
|
|
|
|
|
by other means, and this is the part that matters: **the calendar's states
|
|
|
|
|
differ in shape, not only in colour.** A solid disc, a dotted ring, a continuous
|
|
|
|
|
ring and a star stay tellable apart in greyscale and to a colourblind user, which
|
|
|
|
|
is what §43 actually requires. Colour is the second signal here, never the only
|
|
|
|
|
one.
|
|
|
|
|
|
|
|
|
|
The two rules from §39 that survive unchanged, because they are product
|
|
|
|
|
decisions rather than colour preferences:
|
|
|
|
|
|
|
|
|
|
- **Period state** is a sophisticated rose or plum — never graphic blood-red.
|
|
|
|
|
- **Nothing reads as "safe".** The fertile window is pink rather than green for
|
|
|
|
|
the same reason the labels say "Lower likelihood": this app never gives
|
|
|
|
|
permission.
|
|
|
|
|
|
|
|
|
|
Everything goes through Material 3 colour roles and centralized tokens in
|
|
|
|
|
`core/designsystem`. **No hard-coded colours in a Composable** — a colour
|
|
|
|
|
literal outside the token file is a review failure.
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
|
feat: onboarding, and the Surface that dark mode was missing
Seven screens, §19 and §56 verbatim: welcome, last period, period end, previous
history, the privacy promise, notification privacy, first forecast. Verified end
to end on a device — the flow produces a forecast, the record persists, and a
relaunch goes straight to Today.
Three decisions with tests behind them:
- Nothing is written until the final step. Somebody who abandons onboarding
halfway has not asked this app to remember anything about them.
- Notification privacy is Discreet before the user touches anything (§28), and
Direct is last and never pre-selected. Checked on the device, not only in a
unit test.
- "Still going" and "I'm not sure" both mean no end date. §24: never invent
one. The date picker refuses future dates by not offering them rather than
by rejecting a tap it allowed.
DARK MODE WAS BROKEN FOR ALL OF BATCH 01
PeriodTheme never wrapped its content in a Surface, so every Text without an
explicit colour inherited Material's default — black — and the app background
never painted. In light mode that looked correct by accident, because dark text
on cream is what was wanted anyway. In dark mode the onboarding headings
rendered near-black on charcoal.
No test caught it and no test easily would have. It was found by opening the
app on a device and looking at it.
The Surface now lives in the theme, so a screen without a Scaffold cannot
forget, and every illustration has a light/dark preview pair. A preview is not
a test, but it is the cheapest thing that puts the failure in front of whoever
is editing the screen.
closes #16
2026-08-18 03:44:46 -05:00
|
|
|
## Both themes, every time
|
|
|
|
|
|
|
|
|
|
Dark mode was broken for the whole of Batch 01 and nothing said so. `PeriodTheme`
|
|
|
|
|
was not wrapping its content in a `Surface`, so any `Text` without an explicit
|
|
|
|
|
colour inherited Material's default — black — and the app's own background never
|
|
|
|
|
painted at all. **In light mode that looked correct by accident**, because dark
|
|
|
|
|
text on cream is what was wanted anyway; in dark mode the onboarding headings
|
|
|
|
|
were near-black on charcoal.
|
|
|
|
|
|
|
|
|
|
No test caught it and no test easily would. It was found by opening the app.
|
|
|
|
|
|
|
|
|
|
Two rules follow. Every screen gets a **preview pair, light and dark** — cheap,
|
|
|
|
|
and it puts the failure in front of whoever is editing. And the `Surface` lives
|
|
|
|
|
in the theme rather than in each screen, so a screen without a `Scaffold` cannot
|
|
|
|
|
forget it.
|
|
|
|
|
|
feat: the designed Settings screen, structured as §36 specifies
closes #33
Settings is now a root with §36's sections rather than one screen of notification
toggles that called itself a working surface. Reminders move underneath it as a
child destination; the banner is gone because the claim it made stopped being
true, not because the screen changed.
A ROW FOR AN UNBUILT FEATURE IS ABSENT, NOT DISABLED
§36 lists six sections and this shows the two that have something behind them.
A greyed-out "Export My Data" is a promise with no delivery date, and a disabled
"Delete My Data" tells a user that control over their own health data exists and
that they may not have it. In a product whose whole argument is that the data is
theirs, that is a bad first thing to say. Privacy & Security arrives with #34 to
#37, Appearance with Batch 08, Premium with Batch 07.
ONE COPY OF THE FERTILITY DISCLAIMER
About needs it, and it was already a literal in TodayScreen and CalendarScreen —
a third copy would have made two versions of a safety promise inevitable. Now a
string resource all three read. Notifications still deliberately omit it: a long
sentence about fertility on a lock screen is the leak the discreet copy exists
to prevent.
The version is read from PackageManager rather than BuildConfig, so it reports
what is actually installed — the number a person reads out when something is
wrong.
A DOC TRIGGER THAT COULD NOT FIRE
docs/design/README.md's trigger is "any new user-facing screen or state" while
its Governs listed only docs/design/** and core/designsystem/**, so adding a
screen fired nothing. Same class of defect as the architecture doc's, found the
same way — by adding the thing the trigger is about and watching it not fire.
Widened, and verified by running doc-triggers.py against the new file.
Driven on PeriodMinSdk26: the root renders, rows expand in place, reminders open
as a child, and back returns to the root rather than exiting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:44:15 -05:00
|
|
|
## A row for an unbuilt feature is absent, not disabled
|
|
|
|
|
|
|
|
|
|
Settings is built as `PRODUCT_PLAN.md` §36 structures it, and it shows only the
|
|
|
|
|
sections that have something behind them. Privacy & Security arrives with the
|
|
|
|
|
issues that fill it; Appearance and Premium with their batches.
|
|
|
|
|
|
|
|
|
|
The alternative — every §36 row present and greyed out — is worse here than in
|
|
|
|
|
most apps. A disabled *Export My Data* is a promise with no delivery date, and a
|
|
|
|
|
disabled *Delete My Data* tells a user that the control over their own health
|
|
|
|
|
data exists and that they may not have it. In a product whose entire argument is
|
|
|
|
|
that the data is theirs, that is a bad first thing to say.
|
|
|
|
|
|
|
|
|
|
Absent is honest, and the tracker is where the plan lives — which is the same
|
|
|
|
|
rule [`../data/README.md`](../data/README.md) applies to placeholder images, for
|
|
|
|
|
the same reason: something that looks finished outlives the issue that would
|
|
|
|
|
have replaced it.
|
|
|
|
|
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
## The states most often left undesigned
|
|
|
|
|
|
|
|
|
|
Designed here on purpose, because they are the two most people meet first:
|
|
|
|
|
|
|
|
|
|
- **Empty** — no periods logged yet. It has to make the next action obvious
|
|
|
|
|
rather than apologise.
|
|
|
|
|
- **Learning** — one or two cycles recorded. The app says it is still learning
|
|
|
|
|
rather than showing a confident forecast it has not earned. "Getting to know
|
|
|
|
|
your pattern", not a percentage.
|
|
|
|
|
|
feat: ship the onboarding artwork in both themes
closes #29
All eight illustrations are in the app, every onboarding step has art for the
first time, and the theme swap is resource resolution rather than a branch:
drawable/ and drawable-night/, so Android picks the asset and the artwork follows
a device theme change with nothing of ours running.
NOT VECTORS, AND THE ISSUE WAS WRONG TO ASK FOR THEM
#29 called for a redraw as Compose vector paths taking colour from theme tokens.
That is right for artwork that does not exist yet and wrong for gradient
landscapes with glow and depth — there is no honest VectorDrawable of one, and
the attempt loses the drawing or produces path soup slower than the image. §42
objects to *unnecessary* raster, which this is not.
The cost was never measured before it was argued about: all sixteen files at the
height they are actually drawn at come to 130 KB. Release APK 1.90 -> 2.03 MB.
WHAT ONLY A DEVICE SHOWED
Each drawing is composed as a card with its own rounded corner painted in, so the
pixels outside that curve are the card's own backdrop — near-black in the dark
set. Rendered unclipped, every illustration showed four notches against the app
background. Illustration() now clips just inside the painted curve.
WHAT STAYED A VECTOR
The calendar markers and CycleProgressMark, because they are drawn from data — a
marker's shape is what a day is, and the progress arc is where the user stands in
their cycle. CycleProgressMark moves from Illustrations.kt to CycleMarkers.kt so
the file layout carries that boundary.
Sized by height rather than a square box: the set has no common aspect ratio, and
height is the dimension that decides whether the primary button stays on screen.
Steps 3 and 6 take 104dp because they carry three buttons and three option cards;
verified by driving all seven steps, not by reasoning about it.
OnboardingPreviews.kt now has one entry per illustration, because a missing
drawable-night asset is invisible in light mode.
Driven end to end on PeriodMinSdk26 in both themes, no crash. ./gradlew check,
schema-guard and doc-claims all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:25:59 -05:00
|
|
|
## The artwork is real now, and the replaceable names are why that was cheap
|
|
|
|
|
|
|
|
|
|
Every illustration in the app is the owner's own drawing, in a matched light and
|
|
|
|
|
dark pair, in
|
|
|
|
|
[`core/designsystem/src/main/res/`](../../core/designsystem/src/main/res/). The
|
|
|
|
|
sources and the decisions behind them are in [`dist/`](dist/).
|
|
|
|
|
|
|
|
|
|
**That swap changed one function body per illustration and no screen's layout**,
|
|
|
|
|
because PRODUCT_PLAN.md §42's advice was followed literally when there was
|
|
|
|
|
nothing to draw with: polished placeholders behind replaceable names. The names
|
|
|
|
|
did not change when the drawings did. That is the whole argument for the rule,
|
|
|
|
|
and it is worth keeping for the artwork still to come.
|
|
|
|
|
|
|
|
|
|
**These are raster, and §42 still holds.** Its preference list ends "avoid
|
|
|
|
|
*unnecessary* raster imagery", and it offers placeholder vectors as the fallback
|
|
|
|
|
for artwork that does not exist. The artwork exists, and it is gradient
|
|
|
|
|
landscapes with glow and depth — there is no honest `VectorDrawable` of one. The
|
|
|
|
|
whole set costs 130 KB of the release APK.
|
|
|
|
|
|
|
|
|
|
**The calendar markers stay vectors**, and so does `CycleProgressMark`. They are
|
|
|
|
|
drawn *from data* — a marker's shape is what a day is, and the progress arc is
|
|
|
|
|
where the user actually stands in their cycle. No amount of good artwork makes
|
|
|
|
|
those images.
|
|
|
|
|
|
|
|
|
|
**The theme swap is resource resolution, not a branch.** Each drawable has a
|
|
|
|
|
`drawable-night/` twin, so Android picks it and the artwork follows a device
|
|
|
|
|
theme change with no code of ours involved. The cost is that a missing night
|
|
|
|
|
asset is invisible in light mode, which is why the preview pair in
|
|
|
|
|
`OnboardingPreviews.kt` has one entry per illustration.
|
|
|
|
|
|
feat: the real emblem is the launcher icon
closes #30
The last placeholder. It was blocked on a real constraint rather than taste: an
adaptive icon masks roughly a quarter of the canvas away, and the first emblem
had content hard against its edges, so dropping it in would have cropped the
shield.
THE NEW EMBLEM DID NOT NEED REDRAWING, IT NEEDED PLACING
It puts the meaning in the middle — calendar, shield, padlock — with the ring and
botanicals as outer decoration. Dropped in full-bleed it still crops: simulating
the circular and rounded-square masks slices the ring apart and cuts the crescent
in half. Scaled into the 66dp safe circle on §18's Deep Plum it survives both
masks whole.
Both were checked by rendering the masks, and then by installing and looking at
the launcher rather than trusting the simulation.
Foreground ships as a density ladder, mdpi through xxxhdpi, 44 KB for all five.
Release APK 2.03 -> 2.08 MB.
THE MONOCHROME LAYER STAYS THE SIMPLIFIED VECTOR
Renamed to ic_launcher_monochrome.xml, which is now what it is. A themed icon is
a silhouette — Android discards colour and tints the alpha — so the emblem's
overlapping calendar, shield, padlock, crescent and leaves would flatten into an
unreadable blob. A ring with one dot still reads at 48dp in a single colour,
which is the whole job of that layer.
Source kept at docs/design/brand/icon.webp: same 1254px, 128 KB instead of 896.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:37:11 -05:00
|
|
|
**The launcher icon is the real emblem now too.** It was the last placeholder,
|
|
|
|
|
and it was blocked on a real constraint: an adaptive icon masks roughly a
|
|
|
|
|
quarter of the canvas away, and the first emblem had content hard against its
|
|
|
|
|
edges, so dropping it in would have cropped the shield.
|
|
|
|
|
|
|
|
|
|
The emblem supplied for it solves that by putting the meaning in the middle —
|
|
|
|
|
calendar, shield, padlock — with the ring and botanicals as the outer decoration.
|
|
|
|
|
So it did not need redrawing, it needed *placing*: scaled into the 66dp safe
|
|
|
|
|
circle on the Deep Plum background from `BRAND_GUIDE.md` §18, it survives the
|
|
|
|
|
circular and rounded-square masks whole. Checked by simulating both masks, and
|
|
|
|
|
then by looking at the launcher.
|
|
|
|
|
|
|
|
|
|
**The monochrome layer is still the simplified vector**, deliberately. A themed
|
|
|
|
|
icon is a silhouette — Android discards the colour and tints the alpha — so the
|
|
|
|
|
emblem's overlapping calendar, shield, padlock, crescent and leaves would flatten
|
|
|
|
|
into an unreadable blob. A ring with one dot still reads at 48dp in one colour,
|
|
|
|
|
which is all that layer has to do.
|
docs: correct 57 claims the code and tracker disagreed with
Every document in the tree audited against the source, the tracker and git
history, each finding then given to a second reader tasked with refuting it.
74 raised, 12 refuted, 57 applied. No code changed.
THE README DESCRIBED A SKELETON
Its Status table — the one place a claim about what is built is allowed to live
— still read "there is no usable app yet", with Not built against Room, the four
core screens, fertility and notifications, and No round run against QA. Five
batches had shipped and three QA rounds had run.
TWO DOCUMENTS WERE SILENTLY NEVER FIRING
architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle
module graph", "the design tokens in core/designsystem"). Neither contains a
path token, so doc-triggers.py reduced them to globs matching nothing, and one
real glob apiece made them look path-governing rather than subject-governing —
the state the script's own header calls invisible. Editing a Room entity never
fired the document owning the migration table. Both now fire, proved by running
the script.
SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS
App lock listed among what works offline; biometric/PIN gating described as
protecting app launch; the incognito launcher as existing; Play Billing in the
third parties table without the "not yet integrated" marker its neighbours
carry. All are Batch 06/07 work.
The advertising boundary was overstated in SECURITY.md and the README alike:
both said the ads module declares no dependency and a guard proves it. There is
no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than
the sentence it replaced and matches nothing until Batch 07, which is why the
guard is proved by injection rather than trusted.
SMALLER, EACH A REAL TRAP
WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template
path absent here — missed by doc-claims.sh, which reads backticked prose and not
fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three
rounds because ClaudeQAPlan.md's after-a-round list never named that section;
the playbook is fixed first. The instrumented-test count was eight in three
places and is four. HISTORY.md said the repository had no code and that nothing
had been tried and dropped, when three approaches had.
DELIBERATELY UNCHANGED
ClaudeReport.md's last verified build SHA stays at 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
|
2026-08-18 18:53:25 -05:00
|
|
|
### What the artwork has to do
|
|
|
|
|
|
|
|
|
|
Two things at once, stated by the owner and recorded here because it is the brief
|
|
|
|
|
every remaining art issue is judged against: **welcoming, and visibly private.**
|
|
|
|
|
Not alternating between them screen by screen — a warm opening followed by a bare
|
|
|
|
|
form is how onboarding reads today, and the privacy idea currently appears on
|
|
|
|
|
exactly one screen out of seven.
|
|
|
|
|
|
|
|
|
|
Welcoming is the harder half to get right without breaking [Tone](#tone): warmth
|
|
|
|
|
here means an app that looks like it was made with care, never one that
|
|
|
|
|
celebrates a period or decorates it. Private is already half-solved in the
|
|
|
|
|
vocabulary — `PrivacyIllustration` encloses a ring in a shield, and enclosure
|
|
|
|
|
reads as protection without reaching for a padlock, which would read as a
|
|
|
|
|
security product rather than a calm one.
|
|
|
|
|
|
feat: ship the onboarding artwork in both themes
closes #29
All eight illustrations are in the app, every onboarding step has art for the
first time, and the theme swap is resource resolution rather than a branch:
drawable/ and drawable-night/, so Android picks the asset and the artwork follows
a device theme change with nothing of ours running.
NOT VECTORS, AND THE ISSUE WAS WRONG TO ASK FOR THEM
#29 called for a redraw as Compose vector paths taking colour from theme tokens.
That is right for artwork that does not exist yet and wrong for gradient
landscapes with glow and depth — there is no honest VectorDrawable of one, and
the attempt loses the drawing or produces path soup slower than the image. §42
objects to *unnecessary* raster, which this is not.
The cost was never measured before it was argued about: all sixteen files at the
height they are actually drawn at come to 130 KB. Release APK 1.90 -> 2.03 MB.
WHAT ONLY A DEVICE SHOWED
Each drawing is composed as a card with its own rounded corner painted in, so the
pixels outside that curve are the card's own backdrop — near-black in the dark
set. Rendered unclipped, every illustration showed four notches against the app
background. Illustration() now clips just inside the painted curve.
WHAT STAYED A VECTOR
The calendar markers and CycleProgressMark, because they are drawn from data — a
marker's shape is what a day is, and the progress arc is where the user stands in
their cycle. CycleProgressMark moves from Illustrations.kt to CycleMarkers.kt so
the file layout carries that boundary.
Sized by height rather than a square box: the set has no common aspect ratio, and
height is the dimension that decides whether the primary button stays on screen.
Steps 3 and 6 take 104dp because they carry three buttons and three option cards;
verified by driving all seven steps, not by reasoning about it.
OnboardingPreviews.kt now has one entry per illustration, because a missing
drawable-night asset is invisible in light mode.
Driven end to end on PeriodMinSdk26 in both themes, no crash. ./gradlew check,
schema-guard and doc-claims all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:25:59 -05:00
|
|
|
The onboarding brief has been answered: eight illustrations, both themes, now
|
|
|
|
|
shipping in the app. [`dist/`](dist/) holds the sources and the reasoning — no
|
|
|
|
|
text painted into the pixels, no step numbers, and a forecast illustration that
|
|
|
|
|
names no fields so it cannot contradict the live one beside it.
|
docs: add the onboarding concept cards as references, not assets
Seven cards, one per onboarding step, supplied by the owner and generated from
BRAND_GUIDE.md §22's own master prompt — which is the workflow that section
exists to describe. They settle the direction #29 asked for, including all four
steps that had no art at all.
They are NOT the shipping assets, and docs/design/dist/README.md says so at the
point somebody would otherwise drop them into res/drawable:
- raster, against a 1.9 MB release APK and §42's ask for vector paths
- titles and subtitles painted into the pixels, while the app already renders
all seven through Heading(...) — used as-is every title appears twice, and
baked text cannot be translated, scaled with font size, or read by TalkBack
- light mode only, with dark mode inbound and a preview-pair rule that exists
because dark mode was silently broken for all of Batch 01
- card 7 shows a fertile window and ovulation that the first-forecast screen
deliberately withholds; on a fresh install it shows confidence Low and Today
reads "Not enough history to estimate"
Converted from the delivered PNGs to WebP at the original 1086x1448: same
resolution, 340 KB instead of 12 MB, and the format docs/data/img already uses.
Renamed from the generator's default filenames so the set reads as a sequence.
#29 stays open with the assessment recorded as a comment on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:28:50 -05:00
|
|
|
|
feat: ship the onboarding artwork in both themes
closes #29
All eight illustrations are in the app, every onboarding step has art for the
first time, and the theme swap is resource resolution rather than a branch:
drawable/ and drawable-night/, so Android picks the asset and the artwork follows
a device theme change with nothing of ours running.
NOT VECTORS, AND THE ISSUE WAS WRONG TO ASK FOR THEM
#29 called for a redraw as Compose vector paths taking colour from theme tokens.
That is right for artwork that does not exist yet and wrong for gradient
landscapes with glow and depth — there is no honest VectorDrawable of one, and
the attempt loses the drawing or produces path soup slower than the image. §42
objects to *unnecessary* raster, which this is not.
The cost was never measured before it was argued about: all sixteen files at the
height they are actually drawn at come to 130 KB. Release APK 1.90 -> 2.03 MB.
WHAT ONLY A DEVICE SHOWED
Each drawing is composed as a card with its own rounded corner painted in, so the
pixels outside that curve are the card's own backdrop — near-black in the dark
set. Rendered unclipped, every illustration showed four notches against the app
background. Illustration() now clips just inside the painted curve.
WHAT STAYED A VECTOR
The calendar markers and CycleProgressMark, because they are drawn from data — a
marker's shape is what a day is, and the progress arc is where the user stands in
their cycle. CycleProgressMark moves from Illustrations.kt to CycleMarkers.kt so
the file layout carries that boundary.
Sized by height rather than a square box: the set has no common aspect ratio, and
height is the dimension that decides whether the primary button stays on screen.
Steps 3 and 6 take 104dp because they carry three buttons and three option cards;
verified by driving all seven steps, not by reasoning about it.
OnboardingPreviews.kt now has one entry per illustration, because a missing
drawable-night asset is invisible in light mode.
Driven end to end on PeriodMinSdk26 in both themes, no crash. ./gradlew check,
schema-guard and doc-claims all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:25:59 -05:00
|
|
|
The art still outstanding is tracked as issues #30 to #32, not listed here.
|
2026-08-18 18:53:25 -05:00
|
|
|
|
feat: placeholder vector artwork, and calendar markers that work in greyscale
Compose vector paths in core/designsystem/art — no raster anywhere under src/,
so everything stays crisp at any density and adds nothing to the APK.
The four calendar markers differ in SHAPE, not only colour: a solid disc for a
confirmed period, a dotted outline for a predicted one, a continuous ring for
the fertile window, and a four-pointed mark for ovulation. §26 and §43 both
require it and for the same reason — a calendar whose states differ only in
colour carries no information for a colourblind user, none in greyscale, and
none in the bug report somebody files about it. Predicted is deliberately not a
lighter confirmed: opacity alone fails exactly when the screen is dim.
Illustrations for welcome, empty state, learning and the privacy promise, plus
the cycle progress mark for Today. The visual language is overlapping circular
forms and nothing else — §42's forbidden list is a product decision, not
squeamishness: this app gets opened in public and a glance over a shoulder
should learn nothing.
Placeholders here and never in docs/data/img, which docs/design/README.md now
explains rather than leaving as an apparent inconsistency: §42 asks for these
explicitly, a replaceable name keeps them replaceable, and a screen with no
illustration cannot be evaluated while a project card with no icon just shows
initials.
closes #15
2026-08-18 03:35:04 -05:00
|
|
|
The visual language is deliberately narrow — **overlapping circular forms** and
|
|
|
|
|
nothing else. §42's forbidden list (blood drops, tampons, pads, uterus imagery,
|
|
|
|
|
gender symbols, anatomy) is a product decision rather than squeamishness: this
|
|
|
|
|
app gets opened in public, and somebody glancing over a shoulder should learn
|
|
|
|
|
nothing. That is a property of the pictures as much as of the notification text.
|
|
|
|
|
|
|
|
|
|
**Note the deliberate inconsistency with `docs/data/img/`,** where a placeholder
|
|
|
|
|
is forbidden. The rule there is that an image which looks finished outlives the
|
|
|
|
|
issue that would have replaced it, because nobody files a ticket against
|
|
|
|
|
something that appears done. In-app assets escape that because §42 asks for them
|
|
|
|
|
explicitly and the replaceable-name mechanism is what keeps them replaceable —
|
|
|
|
|
and because a screen with no illustration cannot be evaluated at all, while a
|
|
|
|
|
project card with no icon simply shows initials.
|
|
|
|
|
|
chore: adopt the project template and add the Kotlin/Compose skeleton
Period was a bare directory holding one 2,527-line specification, with no git
repository, no tracker and no documentation convention. This is the adoption
from Projects/Template/START-HERE-New-Project.md, plus a project that compiles
so the hooks and future guards have something real to run against.
Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to
docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the
capitalised Docs/ is gone. Every scaffolded document was filled in for Period.
docs/OPERATIONS.md deleted — an offline app is not a deployed service.
DOC_TRUST_MAP.md written last, describing what is actually here, including what
this project deliberately does not have.
Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm")
and cannot see the Android SDK, so the engine is testable without an emulator —
17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51.
BaselinePredictionEngine is a robust-median prototype and explicitly not the
product; it exists so Batch 02's replacement can be shown to be better rather
than merely different.
Versions verified against their official sources today rather than inherited
from the specification's own numbers, which that document asks for: Kotlin
2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt
2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no
longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk
stays 36, Play's floor from 2026-08-31, and the difference is deliberate.
Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md.
Three hooks in .githooks/, with pre-commit adapted to Gradle.
closes #1
closes #2
2026-08-18 02:16:47 -05:00
|
|
|
## Include the rejected version
|
|
|
|
|
|
|
|
|
|
For any decision that was genuinely close, record what was not chosen and why.
|
|
|
|
|
Without it the same option is proposed again every few months and re-argued from
|
|
|
|
|
nothing. The first entries belong to whoever makes those calls; the
|
|
|
|
|
specification's `Avoid:` lists are already a partial record of them.
|
|
|
|
|
|
|
|
|
|
## What does not belong here
|
|
|
|
|
|
|
|
|
|
- How it is built — [`../architecture/README.md`](../architecture/README.md)
|
|
|
|
|
- Scope and audience — [`../planning/PROJECT_PLAN.md`](../planning/PROJECT_PLAN.md)
|