forked from eva/focus-flow
docs(data): define v1 mongodb schema
This commit is contained in:
parent
61882e5cd3
commit
dcd049c549
2 changed files with 395 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# V1 Block 15 — Persistence Schema, Codecs, and Repository Contracts
|
||||
|
||||
Status: Planned
|
||||
Status: In progress
|
||||
|
||||
Purpose: Turn the current persistence preparation into a complete, versioned,
|
||||
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
|
||||
|
||||
Status: Complete
|
||||
|
||||
Tasks:
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
and migrations.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue