# 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.
