docs(data): define v1 mongodb schema

This commit is contained in:
Ashley Venn 2026-06-25 11:03:17 -07:00
parent 61882e5cd3
commit dcd049c549
2 changed files with 395 additions and 1 deletions

View file

@ -0,0 +1,366 @@
# 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.

View file

@ -1,6 +1,6 @@
# V1 Block 15 — Persistence Schema, Codecs, and Repository Contracts # V1 Block 15 — Persistence Schema, Codecs, and Repository Contracts
Status: Planned Status: In progress
Purpose: Turn the current persistence preparation into a complete, versioned, Purpose: Turn the current persistence preparation into a complete, versioned,
MongoDB-document-friendly V1 data contract before introducing a database client. MongoDB-document-friendly V1 data contract before introducing a database client.
@ -9,6 +9,8 @@ MongoDB-document-friendly V1 data contract before introducing a database client.
Recommended Codex level: high Recommended Codex level: high
Status: Complete
Tasks: Tasks:
- Define the V1 collection/document inventory for tasks, projects/project stats, - Define the V1 collection/document inventory for tasks, projects/project stats,
@ -48,6 +50,32 @@ Acceptance criteria:
- Required indexes are listed with the use cases they support. - Required indexes are listed with the use cases they support.
- The decision on scheduling snapshots versus operation records is explicit. - The decision on scheduling snapshots versus operation records is explicit.
Completed implementation:
- Added `V1_ADR_002_MongoDB_Document_Schema_V1.md` as the accepted Block 15.1
schema decision record.
- Defined the V1 collection inventory for tasks, projects, project statistics,
locked blocks, locked overrides, task activities, owner settings, notice
acknowledgements, operation records, and bounded scheduling snapshots.
- Established common document conventions for `schemaVersion`, `_id`, `ownerId`,
optimistic `revision`, `createdAt`, and `updatedAt`.
- Documented string encodings for instants, civil dates, wall times, time-zone
IDs, intervals, optional null/clear fields, and unknown-field tolerance.
- Replaced long-term enum `.name` persistence with explicit stable code values
and future decode-table requirements.
- Made scheduling snapshots bounded diagnostics and pending-notice carriers
rather than authoritative task/project/locked state.
- Documented archive behavior, internal operation retention, hidden locked-time
privacy boundaries, document-size guards, required indexes, and uniqueness
constraints.
Verification:
- `dart format lib test`: passed, 0 files changed after final format
- `dart analyze`: passed, no issues found
- `dart test`: passed, 277 tests
- `git diff --check`: passed
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing all codecs BREAKPOINT: Stop here. Confirm `extra high` mode before implementing all codecs
and migrations. and migrations.