@specfork/habits
Personal, local-first habit tracker with yes/no and measurable habits, flexible frequencies, skips, streaks, habit strength score, charts, reminders, export and backup.
Habits
One person tracks their own habits on their own device. A habit is either yes/no ("did I do it?") or measurable ("how much?"), repeats daily, every N days, N times per week or month, and earns a strength score that rises with consistency and decays slowly with misses. History, streaks, charts and reminders show progress; all data stays local and can be exported, imported and backed up. Behaviour derived from Loop Habit Tracker's open-source code (GPL-3.0-or-later), written fresh and stack-free. Not affiliated with or endorsed by Álinson Santos Xavier. The name is used only to credit the source.
| Area | Prefix | Covers |
|---|---|---|
| habits.md | HAB | Habit fields, types, frequency, colours, archive, delete, order, sort, filter, search |
| checkins.md | CHK | Days, entries, yes/no/skip/unknown, amounts, notes, toggling, auto-completed days, completion |
| scoring.md | SCR | Habit strength score: formula, windows, measurable targets, skips |
| streaks.md | STK | Streaks, current streak, best streaks |
| charts.md | CHT | Habit list row, overview, history calendar, score, totals, target progress, weekday frequency |
| reminders.md | REM | Reminder time and weekdays, time zones, suppression, actions, snooze |
| data.md | DATA | Local storage, full export, spreadsheet export, import, backups |
| settings.md | SET | Preferences and defaults |
Single user, no account, no server: every rule here runs on one device with no network.
Dates are local calendar dates (YYYY-MM-DD); instants are UTC ISO 8601; names are snake_case.
Charts
Status: Draft · Prefix: CHT
Purpose
How the habit list and each habit's detail page show history and progress.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| CHT-1 | Habit list row: name, a ring filled to today's score (SCR-1), and the states of the most recent days that fit the width, newest first (oldest first when reverse_day_order is on). Measurable cells show the amount; empty for unknown. |
5 columns, today Mar 10 → Mar 10..6 |
| CHT-2 | Detail page: question, frequency in words, target and unit (measurable), reminder time if set; then today's score; change versus 30 and 365 days ago in percentage points; total check-ins (STK-6). | 3/7 → "3 times per week"; today 60%, 30 days ago 45% → "+15" |
| CHT-3 | History calendar: one cell per day from the start day to today in week columns starting first_weekday. Yes/no cells: yes full, auto dimmed, skip hatched, no/unknown empty. Measurable: skip hatched; completed with amount > 0 full; completed with no amount dimmed; amount > 0 but not completed grey; else empty. Days with notes carry a marker. |
3/7 with yes 1, 3, 5 → days 2, 4, 6, 7 dimmed |
| CHT-4 | Tapping a calendar cell for today or a past day edits it (CHK-5 or CHK-6, per one_tap_toggle); future cells do nothing. |
tap tomorrow → no change |
| CHT-5 | Score chart: buckets day|week|month|quarter|year (default week); each point = mean of the daily scores in the bucket. Weeks start first_weekday; quarters start Jan, Apr, Jul, Oct. |
daily scores 0.2, 0.4, 0.6 in one week → point 0.4 |
| CHT-6 | Totals chart: yes/no = number of yes days per bucket (week|month|quarter|year, default week); measurable = sum of amounts per bucket (day|week|month|quarter|year, default day); skip and unknown add 0. |
amounts 3, skip, 4 in one week → 7 |
| CHT-7 | Target progress (measurable): current day (daily habits only), week (daily and weekly habits), month, quarter, year; each shows sum versus target. Targets by target_period: day → daily_target × 1, 7, days in month, 91, days in year; week → target × 1, 4, 13, 52; month → target × 1, 3, 12. Each target minus daily_target × skip days in that period, floored at 0. |
70/week, 2 skips this month → month target 280 − 20 = 260 |
| CHT-8 | Weekday frequency: for each calendar month and weekday, the number of yes days (yes/no) or the sum of amounts (measurable). |
yes on 3 Mondays in March → March/Monday = 3 |
| CHT-9 | Chosen buckets persist across restarts; all charts refresh after any change (CHK-14) and when the date changes. | pass midnight → new day column appears |
Out of scope
- Home-screen widgets and watch faces (platform features; they show the same data as CHT-1, CHT-3, CHT-5..8 and best streaks).
- Celebration animations and haptic feedback.
- Cross-habit dashboards.
Decisions
- Widgets are reduced to "the same views elsewhere"; the source has checkmark, history, score, streak, frequency and target widgets.
Check-ins
Status: Draft · Prefix: CHK
Purpose
Recording what happened on each day, and deriving each day's state and completion from the entries and the habit's frequency or target.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| CHK-1 | A day is a local calendar date. today = the device's current local date; with late_day_start on (SET-1), times 00:00–02:59 still belong to the previous date. |
02:30 on 2026-03-02, late_day_start on → today 2026-03-01 |
| CHK-2 | Entry: {habit_id, date, value, notes}, at most one per (habit_id, date); writing an entry for a date replaces the previous one. notes is trimmed free text and may sit on an entry whose value is unknown. |
set yes then no on 2026-03-01 → one entry, no; notes " sore knee " → "sore knee" |
| CHK-3 | Yes/no value: yes|no|skip|unknown. Measurable value: an amount (decimal 0..1,000,000, rounded to 3 decimals), skip or unknown. Writing unknown with empty notes removes the entry. |
amount 2.34567 → stored 2.346; −1 → rejected |
| CHK-4 | Entries may be written for today or any past date, never a future date. | write for tomorrow → rejected; for 2019-05-01 → saved |
| CHK-5 | Quick toggle (yes/no habits) moves a day to the next value and keeps its notes: yes → skip if skip_enabled, else no; skip → no; no → unknown if unknown_enabled, else yes; unknown → yes; an auto day → yes. |
both settings off: yes → no → yes; both on: yes → skip → no → unknown → yes |
| CHK-6 | Entry editor sets value and notes together: yes/no offers yes, no, and skip / unknown only when enabled; measurable offers an amount (blank = unknown) plus the same options. Closing without confirming saves changed notes only. Invalid input keeps the old value. | amount "abc" → previous amount kept |
| CHK-7 | Yes/no day states: yes (entered), auto (covered by frequency, CHK-8), no, skip, unknown. A day with no entry is unknown; when unknown_enabled is off it is shown as no. |
no entry, setting off → shown as no |
| CHK-8 | Covered windows (yes/no, frequency n/d, d > 1): take yes days newest first; for each run of n consecutive ones, if newest − oldest < L days, the window [oldest, oldest + L − 1] is covered. L = d, except d = 30 or 31: L = length of the oldest day's month, or of the next month when the oldest day is its month's last day. | 3/7, yes on days 1, 3, 5 → window days 1–7; 2/7, yes on days 1 and 9 → no window |
| CHK-9 | Windows are adjusted newest to oldest: when a window ends on or after the start of the next newer window, it moves earlier by min(end − newer_start + 1, end − its newest yes day) days. A window may then start before the first yes. | 1/7, yes on 2026-01-01 and 2026-01-05 → windows 2025-12-29..2026-01-04 and 2026-01-05..01-11 |
| CHK-10 | In a covered window, days that are no, unknown or empty become auto; yes and skip stay. Every-day (1/1) habits never have auto days. |
3/7, yes 1, 3, 5, explicit no on day 6 → day 6 auto |
| CHK-11 | A yes/no day is completed when its state is yes, auto or skip. |
skip day → completed |
| CHK-12 | Measurable completion is per period: day (1/1), week starting first_weekday (1/7) or calendar month (1/30). daily_target = target_value ÷ denominator; period_target = max(0, target_value − daily_target × skip days in period); sum = amounts in period (skip, unknown = 0). Met when sum ≥ period_target (at_least) or sum ≤ period_target (at_most). |
70 km/week, Mon 20, Tue 30, Wed skip → target 60, sum 50 → not met; Thu 10 → met |
| CHK-13 | Every day of a met period, from the habit's first entry up to today, is completed; days of other periods are not. "Completed today" = today is completed; "entered today" = today's value is not unknown. |
at most 2 coffees/day, entries on earlier days, none today → completed today, not entered today |
| CHK-14 | Day states, completion, scores and streaks are re-derived right after any entry or habit change. | toggle yesterday to yes → score and streak change at once |
Out of scope
- Multiple check-ins per day with times of day; geolocation or photo proof.
- Automatic check-ins from other apps or devices (health, fitness sensors).
Decisions
- Covered windows reproduce the source exactly, including an explicit
noinside a window becomingautoand windows reaching back before the first yes; this keeps streaks as long as the frequency allows. - Amount limit 1,000,000 replaces the source's implicit integer-overflow ceiling (about 2.1 million).
- Removing an
unknownentry with no notes is equivalent to the source keeping an empty row.
Data
Status: Draft · Prefix: DATA
Purpose
Where data lives, exactly what "export all my data" returns, spreadsheet export, import, and automatic backups.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| DATA-1 | Local-first, single user: all data lives on the device in one data set; no account, sign-in or network is needed or used. Data leaves the app only through a user-started export or backup, to a location the user picks. | airplane mode → every feature works; no network requests |
| DATA-2 | Full export ("export all my data") is one UTF-8 JSON file habits-export-YYYY-MM-DD-HHmmss.json (local time): {format: "spechub-habits", version: 1, exported_at (UTC), settings ([SET-1](#SET-1)), habits: [...]}, with every habit, archived ones included, in position order. |
3 habits, 1 archived → habits has 3 items |
| DATA-3 | Each habit in the export: {id, name, question, notes, type, color, frequency {numerator, denominator}, unit, target_value, target_type, archived, position, created_at, reminder {hour, minute, weekdays: ["mon".."sun"]} | null, entries: [{date, value, notes}]}. Text fields are "" when empty; unit, target_value, target_type are null for yes/no habits. |
yes/no habit → "unit": null |
| DATA-4 | entries are the stored entries only (CHK-2), oldest first; yes/no value is "yes"|"no"|"skip"|"unknown", measurable value is a number or "skip"|"unknown". Derived states, auto days, scores and streaks are not exported. |
amount 2.5 → "value": 2.5; covered day absent |
| DATA-5 | Spreadsheet export: a zip habits-csv-YYYY-MM-DD.zip with habits.csv (one row per habit, fields of DATA-3 except entries and reminder, frequency as numerator, denominator), days.csv (habit_id, date, state, amount, completed, notes, one row per habit per day from start day to today) and scores.csv (habit_id, date, score with 4 decimals). UTF-8, comma-separated, quoted per RFC 4180, header row first. |
daily habit started 10 days ago → 11 days.csv rows |
| DATA-6 | Import accepts a full export or backup with format "spechub-habits" and version ≤ the app's; any other file is rejected with a message. Import is all-or-nothing. |
version: 2 → rejected, data unchanged |
| DATA-7 | Import merges by habit id: a known habit takes the imported fields except position; an unknown habit is created at the end of the manual order. Entries are written per (habit, date) where value or notes differ; habits and entries missing from the file are kept. |
file has habit A with 1 changed entry → A updated, only that entry changes |
| DATA-8 | Settings from the file are applied only when the app had no habits before the import. | fresh install, import → settings restored; existing data → settings unchanged |
| DATA-9 | Round trip: export, then import into an empty app, gives identical habits, entries and settings, hence identical scores and streaks. Importing the same file twice changes nothing the second time. | compare exports before and after → equal except exported_at |
| DATA-10 | Backup = a full export (DATA-2) named habits-backup-YYYY-MM-DD-HHmmss.json, written to the backup folder: a user-chosen folder, else the app's private storage. "Back up now" writes one at once; restoring = importing a chosen backup (DATA-6..8). |
back up now → new file; restore it into a fresh install → all habits back |
| DATA-11 | Automatic backup: each time the app opens or returns to the foreground, if the newest backup is older than 24 hours or none exists, write one; then keep only the newest 5 files matching the backup name pattern in that folder. | 5 backups, newest 25 h old, open app → 6th written, oldest deleted |
Out of scope
- Sync between devices or with a server; accounts (
@specfork/authis not required). - Importing other apps' formats (the source reads three other habit apps' files and its own database file).
- Cloud backup services; encryption of export files.
Decisions
- The export is a documented JSON file instead of the source's raw database copy, so a user can read it and any rebuild can import it; the source's CSV zip becomes DATA-5 with long-format tables.
- Imported new habits go to the end of the list; the source copied their stored position, which can collide.
- Export, import and backup stay here instead of
@specfork/privacy: PRV needs a signed-in user (@specfork/auth:AUTH-23re-auth, per-user download links, email confirmation) and requires@specfork/auth, which this account-free app does not use; and PRV-5 never merges, while DATA-7 merges by id so a backup restores in place. - Two devices can be kept in step by hand through export/import, since ids are stable; real sync is left out to keep the app account-free.
Habits
Status: Draft · Prefix: HAB
Purpose
What a habit is, how its frequency and target are set, and how the habit list is archived, deleted, ordered, sorted, filtered and searched.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| HAB-1 | Habit: {id (uuid, 32 lowercase hex, generated), name, question?, notes?, type: yes_no|measurable, color (0..19), frequency {numerator, denominator}, unit?, target_value?, target_type?: at_least|at_most, archived (default false), position, reminder? (REM-1), created_at}. |
create "Read" → 32-hex id, archived false |
| HAB-2 | type is chosen at creation and never changes; unit, target_value, target_type exist only for measurable habits and are empty for yes/no habits. |
edit yes/no habit with type: measurable → rejected |
| HAB-3 | name, question, notes, unit are trimmed on save. name is required, 1..50 characters after trimming; the others have no limit. |
name " Run " → "Run"; name " " → rejected; 51 chars → rejected |
| HAB-4 | Yes/no frequency choices and stored value: every day → 1/1; every N days → 1/N (N 1..999); N times per week → N/7 (N 1..9); N times per month → N/30 (N 1..99); X times per Y days → X/Y (1..999 each). Defaults to 1/1. Numerator < 1, a blank part, or numerator ≥ denominator stores 1/1; a denominator of 30 or 31 is shown as "per month". | "3 times per week" → 3/7; "7 times per week" → 1/1 |
| HAB-5 | Measurable habit: target_value required, decimal ≥ 0 (default 0); target_type default at_least; target_period: day|week|month stores frequency 1/1, 1/7 or 1/30 (default day). |
target 10 km per week → frequency 1/7, target_value 10 |
| HAB-6 | color is a palette index: 0 red, 1 deep orange, 2 orange, 3 amber, 4 yellow, 5 lime, 6 light green, 7 green, 8 teal, 9 cyan, 10 light blue, 11 blue, 12 indigo, 13 deep purple, 14 purple, 15 pink, 16 brown, 17 dark grey, 18 grey, 19 light grey. New habits default to 11 and get position = number of existing habits (end of the manual order). |
3 habits exist, create one → color 11, position 3 |
| HAB-7 | Editing a habit keeps all its entries; changes to frequency, target or target type re-derive day states, completion, scores and streaks for every past day (CHK, SCR). | daily with yes on Mon, Wed → change to 1/7 → Tue now auto-completed |
| HAB-8 | Archive and unarchive apply to one or more selected habits. An archived habit keeps its entries, is hidden unless show_archived is on, gets no reminders, and is included in exports. |
archive "Read" → gone from list; show_archived on → shown, marked archived |
| HAB-9 | Delete applies to one or more habits after a confirmation that it cannot be undone; it removes the habits, all their entries and any pending reminder. No undo. | delete "Read" → confirm → habit and its 40 entries gone; cancel → unchanged |
| HAB-10 | Manual reorder is possible only when the list is sorted manually: moving a habit from index i to j shifts the habits between them by one, and positions stay contiguous 0..n−1. | order A,B,C,D; move D to index 1 → A,D,B,C, positions 0..3 |
| HAB-11 | Sort orders: manual (by position), name, color (palette index), score (today's score), status. Choosing the current order again reverses it. First choice direction: name, color, status ascending; score descending. The previous order becomes the tie-breaker. Default manual, tie-breaker name ascending. Order, tie-breaker and filters (HAB-13) persist across restarts. |
sort by score → highest first; choose score again → lowest first |
| HAB-12 | status descending: habits completed today first, then measurable before yes/no, then today's value high to low. status ascending is the exact reverse. |
A done, B not done, sort by status (first choice) → B, A |
| HAB-13 | Filters: show_archived (default false), show_completed (default true). With show_completed off, hide habits completed today (CHK-13); when unknown_enabled is on, hide habits with any value entered today instead. |
show_completed off, A done today → A hidden |
| HAB-14 | Search keeps habits whose name, question or notes contain the trimmed query, case-insensitive; an empty query keeps all. | query " run " → "Morning Run" kept, "Read" hidden |
Out of scope
- Habit groups or categories, sub-habits, shared or public habits.
- Habit templates and onboarding hints.
Decisions
- Blank-after-trim names are rejected; the source checked the untrimmed text and could save an empty name.
- Habit type is fixed after creation, as in the source (it is chosen before the form opens).
created_atis added for export clarity; the source has no creation time.
Reminders
Status: Draft · Prefix: REM
Purpose
Local notifications that prompt for a habit at a chosen local time on chosen weekdays, with quick answers and snooze. No server is involved.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| REM-1 | Reminder (optional, default off): {hour 0..23, minute 0..59, weekdays: set of mon..sun}. An empty weekday set means every day. A new reminder defaults to 08:00, every day. |
turn on → 08:00 all days; clear all weekdays → every day |
| REM-2 | A reminder fires at its hour:minute in the device's current time zone on each selected weekday, within 1 minute. | 07:30 Mon–Fri → fires Fri 07:30, not Sat |
| REM-3 | When the device time zone or clock changes, the next fire is recomputed for the new local time. Daylight saving: a skipped local time fires at the first minute after the gap; a repeated one fires once, at its first occurrence. | move Berlin → New York → 08:00 fires 08:00 New York time; 02:30 on a 02:00→03:00 day → fires 03:00 |
| REM-4 | No notification for an archived habit, an unselected weekday, or a habit already completed that day, except at_most habits, which are always reminded. |
yes/no habit done at 07:00, reminder 08:00 → nothing |
| REM-5 | Notification: title = habit name; text = the question, or a generic "did you do it today?" prompt when empty; opening it shows the habit. Actions: yes/no habits → yes, no; measurable → enter amount; all → snooze. |
blank question → generic prompt |
| REM-6 | An action writes the entry for the date the reminder fired for, not the date it is tapped. | 23:50 reminder, tap yes at 00:10 → entry for previous date |
| REM-7 | At most one notification per habit; it is dismissed when any entry is written for that habit on that date from anywhere, or the habit is deleted. | check habit in the list → its notification disappears |
| REM-8 | Snooze: 15, 30, 60, 120, 240, 480 or 1440 minutes, or a chosen clock time (its next occurrence). Snooze dismisses the notification and replaces only the next fire; it is dropped if its time has passed. | snooze 30 at 08:00 → fires 08:30, next day back to 08:00 |
| REM-9 | With sticky_reminders on, a notification dismissed without an action reappears at once; off by default. |
setting on, swipe away → shown again |
| REM-10 | Reminders are rescheduled on app start and device restart; a reminder whose time passed while the device was off is not fired late. | device off 07:00–09:00, reminder 08:00 → no notification at 09:00 |
Out of scope
- Push notifications from a server, email or SMS reminders.
- Location- or context-based reminders; multiple reminder times per habit.
- Platform permission flows (exact-alarm, notification permission) beyond: if denied, the habit shows that reminders are off.
Decisions
- Time-zone and clock-change rescheduling (REM-3) is new: the source kept a scheduled alarm's absolute instant until the next restart or edit.
- Reminders are local and not
@specfork/notifications: that base is per account and delivered by a server. - Snooze is offered everywhere; the source hid it on newer platform versions.
Scoring
Status: Draft · Prefix: SCR
Purpose
The habit strength score: one number per habit per day that rises with consistent repetition and decays slowly with misses, so one missed day after a long run barely hurts.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| SCR-1 | Each habit has a score in [0, 1] for every day from its start day to today. Start day = the oldest day with a derived state (CHK-7, including covered windows); days before it and habits with no entries score 0. Shown as a whole percent, rounded half-up. | score 0.4740 → "47%"; no entries → 0% |
| SCR-2 | Recurrence: score_d = score_(d−1) × m + p_d × (1 − m), with m = 0.5 ^ (√f ÷ 13), f = numerator ÷ denominator of the stored frequency, p_d in [0, 1] the day's progress. The score before the start day is 0, except measurable at_most habits, which start from 1. |
daily: m = 0.948078, first day yes → 0.051922; weekly (1/7): m = 0.980049 |
| SCR-3 | A skip day keeps the previous day's score and is left out of every window below. |
daily: yes then skip → 0.051922 on both days |
| SCR-4 | Window: the last W non-skip days up to and including day d, counting only days on or after the start day. Yes/no: W = denominator and n = numerator; when f < 1, W = 2 × denominator and n = 2 × numerator. Measurable: W = denominator. | 1/7 → W 14, n 2; 3/7 → W 14, n 6; 1/1 → W 1, n 1 |
| SCR-5 | Yes/no progress: p_d = min(1, number of yes days in the window ÷ n). auto days count 0. |
daily, 13 yes in a row from start → 0.500000; then one no → 0.474039 |
| SCR-6 | Measurable at_least progress: p_d = min(1, sum ÷ target_value), sum = amounts in the window (unknown = 0); target_value 0 → p_d = 1. |
daily target 10, first day 5 → 0.025961 |
| SCR-7 | Measurable at_most progress: p_d = clamp(2 − sum ÷ target_value, 0, 1); with target_value 0, p_d = 1 when sum = 0, else 0. |
daily max 10, first day 15 → 0.974039; first day 25 → 0.948078 |
| SCR-8 | Scores for all days are recomputed from the start day after any entry or habit change (CHK-14). | change frequency 1/1 → 3/7 → every past score uses m = 0.965697 |
| SCR-9 | Reference values (daily yes/no, every day yes from the start): day 1 = 5.2%, day 13 = 50.0%, day 30 = 79.8%. Weekly habit, one yes on the start day = 1.0%. | compute → 0.051922, 0.500000, 0.798017; weekly 0.009975 |
Out of scope
- Configurable score formula or half-life.
- Comparing scores across habits beyond sorting (HAB-11).
Decisions
- The doubled window for non-daily yes/no habits is kept from the source: it smooths uneven schedules such as a weekly habit done on different weekdays.
- Measurable habits use a rolling window of
denominatordays for the score but calendar periods for completion (CHK-12), as in the source. - The source computes scores up to 30 days past today; only days up to today are specified because later days are never shown.
Settings
Status: Draft · Prefix: SET
Purpose
The user's preferences, their defaults and what each one changes.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| SET-1 | Settings and defaults: first_weekday: mon..sun (device locale), skip_enabled false, unknown_enabled false, reverse_day_order false, one_tap_toggle false, late_day_start false, theme: system|light|dark (system), sticky_reminders false, reminder_sound (system default), backup_folder (none = private storage), plus list order and filters (HAB-11) and chart buckets (CHT-9). All persist across restarts and are part of the full export (DATA-2). |
fresh install, US locale → first_weekday sun; theme dark → export has "theme": "dark" |
| SET-2 | first_weekday sets where weeks start for measurable completion, the history calendar and weekly chart buckets. |
70/week, switch mon → sun → week sums regroup |
| SET-3 | one_tap_toggle off: tapping a yes/no day opens the entry editor and a long press toggles; on: the reverse. Measurable days always open the editor. |
setting off, tap → editor opens |
| SET-4 | Turning skip_enabled or unknown_enabled off only removes those choices from toggling and the editor; existing skip and unknown entries stay and keep counting. |
skip day exists, turn skip off → still hatched and completed |
| SET-5 | Changing late_day_start or first_weekday re-derives today and all periods at once, without restart. |
01:00, turn late_day_start on → today becomes yesterday's date |
Out of scope
- Per-habit display settings, custom colour palettes, languages beyond following the device.
Decisions
late_day_startkeeps the source's fixed 03:00 cutoff; the source needed a restart to apply it, here it applies at once.
Streaks
Status: Draft · Prefix: STK
Purpose
Runs of consecutive completed days: the current one and the best ones.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| STK-1 | Streak: {start, end, length} = a maximal run of consecutive completed days (CHK-11, CHK-13) on or before today; length = end − start + 1 days. |
completed Mar 1–5 and Mar 8 → streaks of 5 and 1 |
| STK-2 | Skip days and auto days are completed, so they extend a streak. |
daily: yes, skip, yes → one streak of 3 |
| STK-3 | Current streak = the streak ending today; if today is not completed yet, the streak ending yesterday; otherwise 0. | completed Mar 1–5, today Mar 6 not entered → current 5; today Mar 7 → 0 |
| STK-4 | Best streaks: the 10 longest streaks; on equal length the one ending later ranks higher; listed newest end first, each with start, end and a bar scaled to the longest. | 12 streaks → 10 shown by end date descending; lengths 10, 5 → bars 100%, 50% |
| STK-5 | Streaks are recomputed after any entry or habit change (CHK-14). | set Mar 6 to no in a 10-day run → streaks 5 and 4 |
| STK-6 | Total check-ins: yes/no habits count yes days (not auto or skip); measurable habits count days with an amount > 0. |
3/7 with yes on days 1, 3, 5 → total 3, longest streak 7 |
Out of scope
- Streak freezes, rewards, badges or other gamification.
Decisions
- "Current streak" tolerates a not-yet-entered today; the source only lists streaks and has no current-streak value.
- Measurable totals count days with an amount > 0; the source counted a raw stored marker that does not apply to measurable habits.
From Specfork/habits at fe536fc, licence CC-BY-4.0.