# V1 ADR 002: MongoDB Document Schema V1 Status: Accepted during Block 15.1 Date: 2026-06-25 ## Context MongoDB is the committed persistence target for this project. Blocks 11-14 added the pure Dart domain model, repository boundaries, application unit of work, Today/Backlog read models, management commands, notice acknowledgement, and app-open rollover recovery. Block 15 needs one versioned document contract before codecs, migrations, or a runtime MongoDB adapter are implemented. This ADR defines that contract without adding a database client, connection string, sync behavior, accounts, or background services. ## Decision V1 stores application data in MongoDB-friendly documents behind repository interfaces. The core scheduling logic remains independent from MongoDB APIs. Every top-level document uses: - `schemaVersion`: integer, `1` for this contract. - `_id`: stable document id, unique within its collection. - `ownerId`: authorization-neutral owner scope. This is a data boundary, not an account/auth implementation. - `revision`: positive integer for mutable authoritative documents. Starts at `1` and increments on compare-and-set saves. Append-only documents use `revision: 1`. - `createdAt`: UTC instant string. - `updatedAt`: UTC instant string. Append-only documents set this equal to `createdAt`. The adapter may use BSON dates internally later, but the core V1 codec contract uses the existing string conventions: - Instant: UTC ISO-8601 string from `DateTime.toUtc().toIso8601String()`. - Civil date: `YYYY-MM-DD` string with no timezone conversion. - Wall time: `HH:MM` string with no date or timezone conversion. - Time zone: IANA-style string chosen by the application boundary. - Interval: embedded object `{start, end, label?}` where `start` and `end` are UTC instant strings. - Optional fields: V1 writers include known optional fields with `null` when cleared. Decoders tolerate unknown extra fields and ignore them. ## Collections ### `tasks` Authoritative task documents. Required fields: - common fields - `title` - `projectId` - `type` - `status` - `priority` - `reward` - `difficulty` - `durationMinutes` - `scheduledStart` - `scheduledEnd` - `actualStart` - `actualEnd` - `completedAt` - `parentTaskId` - `backlogTags` - `reminderOverride` - `stats` - `backlogEnteredAt` - `backlogEnteredAtProvenance` `backlogEnteredAt` is nullable in V1. V0 migration may set it from `createdAt` with provenance `approximated_from_created_at`; new V1 backlog transitions should set provenance `recorded`. Task documents do not embed history, children, scheduling changes, or notice lists. Child ownership is represented by `parentTaskId`. ### `projects` Authoritative project configuration documents. Required fields: - common fields - `name` - `colorKey` - `defaultPriority` - `defaultReward` - `defaultDifficulty` - `defaultReminderProfile` - `defaultDurationMinutes` - `archivedAt` Archiving a project sets `archivedAt` and increments `revision`. It never deletes or rewrites task history. ### `project_statistics` Project-level aggregate documents derived from internal activities. Required fields: - common fields - `projectId` - `completedTaskCount` - `durationMinuteCounts` - `completionTimeBucketCounts` - `totalPushesBeforeCompletion` - `completedAfterPushCount` - `rewardCounts` - `difficultyCounts` - `reminderProfileCounts` - `appliedActivityIds` `appliedActivityIds` is bounded by compaction policy in future adapter work. It prevents double-application of completion activities. ### `locked_blocks` Authoritative recurring or one-off locked-time definitions. Required fields: - common fields - `name` - `startTime` - `endTime` - `date` - `recurrence` - `hiddenByDefault` - `projectId` - `archivedAt` Archiving a locked block sets `archivedAt`, increments `revision`, and stops base occurrence expansion. It does not delete one-day overrides. ### `locked_overrides` Date-scoped locked-time override documents. Required fields: - common fields - `lockedBlockId` - `date` - `type` - `name` - `startTime` - `endTime` - `hiddenByDefault` - `projectId` Override `type` is one of `remove`, `replace`, or `add`. Overrides are append/update records, not edits to the recurring block. ### `task_activities` Append-only internal activity facts. Required fields: - common fields with `revision: 1` - `operationId` - `code` - `taskId` - `projectId` - `occurredAt` - `metadata` Activities are internal application data. They are not a visible per-task history panel in V1. ### `owner_settings` One owner settings document per owner. Required fields: - common fields - `timeZoneId` - `dayStart` - `dayEnd` - `compactModeEnabled` - `backlogStaleness` Changing `timeZoneId`, `dayStart`, or `dayEnd` can reinterpret local dates. The application layer must return typed warnings or run an explicit migration path; codecs must not silently shift existing instants. ### `notice_acknowledgements` Owner-scoped consumed-notice records. Required fields: - common fields with `revision: 1` - `noticeId` - `acknowledgedAt` The unique key is `(ownerId, noticeId)`. Acknowledgement hides a notice from Today state without mutating the original operation/snapshot record. ### `operation_records` Exactly-once operation records. Required fields: - common fields with `revision: 1` - `operationId` - `operationName` - `committedAt` The unique key is `(ownerId, operationId)`. These records support application idempotency and are not a replacement for task/activity facts. ### `scheduling_snapshots` Bounded diagnostics and pending-notice carrier documents. Required fields: - common fields with `revision: 1` - `operationName` - `sourceDate` - `targetDate` - `window` - `tasks` - `lockedIntervals` - `requiredVisibleIntervals` - `notices` - `changes` - `overlaps` - `retentionExpiresAt` Full scheduling snapshots remain in V1 as bounded diagnostics and as the carrier for pending rollover notices. They are not the authoritative source of task state, project state, locked-time definitions, or activity/statistics facts. Production adapters should keep snapshots compact: - retain no more than 100 embedded tasks per snapshot; - retain no more than 100 changes or overlaps per snapshot; - retain no more than 20 notices per snapshot; - omit or redact hidden locked names from `lockedIntervals`; - set `retentionExpiresAt` after all notices are acknowledged or after the configured diagnostic retention window. If an operation needs more diagnostic data than these bounds allow, it should store the authoritative entity changes and a truncated snapshot with a typed `truncated: true` marker. ## Stable Codes V1 documents must not depend on Dart enum source `.name` values. Codecs will use explicit encode/decode tables. Unknown codes fail closed with typed mapping errors. Initial code values: | Domain enum | Codes | |---|---| | `TaskType` | `flexible`, `inflexible`, `critical`, `locked`, `surprise`, `free_slot` | | `TaskStatus` | `planned`, `active`, `completed`, `missed`, `cancelled`, `no_longer_relevant`, `backlog` | | `PriorityLevel` | `very_low`, `low`, `medium`, `high`, `very_high` | | `RewardLevel` | `not_set`, `very_low`, `low`, `medium`, `high`, `very_high` | | `DifficultyLevel` | `not_set`, `very_easy`, `easy`, `medium`, `hard`, `very_hard` | | `ReminderProfile` | `silent`, `gentle`, `persistent`, `strict` | | `BacklogTag` | `wishlist` | | `LockedWeekday` | `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday` | | `LockedBlockOverrideType` | `remove`, `replace`, `add` | | `TaskActivityCode` | `completed`, `missed`, `cancelled`, `no_longer_relevant`, `manually_pushed`, `automatically_pushed`, `moved_to_backlog`, `restored_from_backlog`, `activated` | | `SchedulingNoticeType` | `info`, `moved`, `overlap`, `no_fit`, `overflow` | | `SchedulingIssueCode` | `task_not_found`, `invalid_task_state`, `missing_duration`, `missing_scheduled_slot`, `non_positive_duration`, `no_available_slot`, `unfinished_tasks_could_not_fit`, `no_unfinished_flexible_tasks`, `duplicate_surprise_log` | | `SchedulingMovementCode` | `backlog_task_inserted`, `flexible_task_moved_to_make_room`, `flexible_task_pushed_to_next_available_slot`, `flexible_task_moved_to_tomorrow`, `unfinished_flexible_tasks_rolled_over`, `flexible_task_moved_to_backlog`, `required_commitment_scheduled` | | `SchedulingConflictCode` | `flexible_task_overlaps_blocked_time`, `surprise_task_overlaps_required_visible_time`, `required_commitment_overlaps_protected_free_slot` | | `ProjectCompletionTimeBucket` | `overnight`, `morning`, `afternoon`, `evening`, `night` | Codec tables may add newer codes in later schema versions. They must not remap an existing code to a different semantic meaning. ## Indexes And Uniqueness Required index specifications are adapter-neutral in V1 and are created in Block 16. | Collection | Index | Purpose | |---|---|---| | all collections | unique `(_id)` | Stable document identity | | all owner-scoped collections | `(ownerId, _id)` | Owner boundary lookup | | `tasks` | `(ownerId, status, scheduledStart, scheduledEnd)` | Today/window and lifecycle queries | | `tasks` | `(ownerId, status, projectId)` | backlog/project filters | | `tasks` | `(ownerId, parentTaskId)` | child task lookup | | `tasks` | `(ownerId, status, backlogEnteredAt, createdAt)` | backlog candidate ordering | | `projects` | `(ownerId, archivedAt, name)` | active project pickers | | `project_statistics` | unique `(ownerId, projectId)` | aggregate lookup | | `locked_blocks` | `(ownerId, archivedAt, date)` | one-off locked expansion | | `locked_blocks` | `(ownerId, archivedAt, recurrence.weekdays)` | recurring locked expansion | | `locked_overrides` | `(ownerId, date, lockedBlockId, type)` | date-scoped override expansion | | `task_activities` | unique `(ownerId, _id)` | append-only activity identity | | `task_activities` | `(ownerId, operationId)` | command idempotency/debug lookup | | `task_activities` | `(ownerId, taskId, occurredAt)` | task activity loading | | `task_activities` | `(ownerId, projectId, code, occurredAt)` | project-stat aggregation | | `owner_settings` | unique `(ownerId)` | one settings document per owner | | `notice_acknowledgements` | unique `(ownerId, noticeId)` | notice consume idempotency | | `operation_records` | unique `(ownerId, operationId)` | exactly-once command boundary | | `scheduling_snapshots` | unique `(ownerId, sourceDate, operationName)` for rollover-style operations | source-day idempotency | | `scheduling_snapshots` | `(ownerId, window.start, window.end)` | Today pending notice lookup | | `scheduling_snapshots` | `(retentionExpiresAt)` | bounded diagnostic cleanup | Partial indexes should prefer active records where applicable, such as `archivedAt: null` for project and locked-block picker/expansion queries. ## Archive, Delete, And Retention Normal user-facing removal uses lifecycle state or archive fields: - Tasks are not hard-deleted by automatic scheduling. `cancelled`, `no_longer_relevant`, and `backlog` preserve history. - Projects use `archivedAt`; task `projectId` values remain unchanged. - Locked blocks use `archivedAt`; overrides remain independent. - Scheduling snapshots are bounded diagnostics and may expire after retention rules once they are no longer carrying pending notices. Hard delete is reserved for explicit adapter/admin operations outside the normal V1 app flow. Operation records and task activities are internal operation data. V1 retains them long enough to preserve idempotency, statistics, and migration safety. Adapter-level pruning must not remove authoritative task/project/locked state or break exactly-once guarantees for active retry windows. ## Privacy Boundaries Documents must not contain credentials, database connection strings, OAuth tokens, sync tokens, or platform-notification secrets. Hidden locked-time details are sensitive. They may live in the owner-scoped `locked_blocks` and `locked_overrides` documents, but they should not be copied into denormalized notices, public summaries, or unnecessary snapshot labels. When snapshots need locked intervals for diagnostics, hidden labels should be redacted or replaced with source IDs and `hiddenByDefault: true`. Internal statistics and activity records are implementation data for future reports and scheduling correctness. They should remain behind repository/use-case boundaries and should not become a visible task-history UI in V1. ## Consequences - Chunk 15.2 must replace the current enum `.name` mapping helpers with explicit stable code tables. - Chunk 15.2 must add codecs for every collection listed above, including owner-scoped notices and bounded scheduling snapshots. - Chunk 15.3 must migrate V0 task documents to V1 with explicit provenance for approximated backlog entry time. - Chunk 15.4 must expand repository contracts so Block 14 use cases can use owner-scoped, indexed query methods instead of broad `findAll()` loading. - Chunk 15.5 must turn the index table in this ADR into tested adapter-neutral index specifications.