focus-flow/Codex Documentation/Current Software Plan/V1_ADR_002_MongoDB_Document_Schema_V1.md

13 KiB

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.