diff --git a/Codex Documentation/Current Software Plan/V1_ADR_002_MongoDB_Document_Schema_V1.md b/Codex Documentation/Current Software Plan/V1_ADR_002_MongoDB_Document_Schema_V1.md new file mode 100644 index 0000000..d7505f9 --- /dev/null +++ b/Codex Documentation/Current Software Plan/V1_ADR_002_MongoDB_Document_Schema_V1.md @@ -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. diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_15_Persistence_Schema_Codecs_Repositories.md b/Codex Documentation/Current Software Plan/V1_BLOCK_15_Persistence_Schema_Codecs_Repositories.md index c1165a1..b01cce9 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_15_Persistence_Schema_Codecs_Repositories.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_15_Persistence_Schema_Codecs_Repositories.md @@ -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.