1.5 Add actor_child_id to completion_events (D-008) #49

Member

Implements IMPLEMENTATION-ORDER.md §1.5 (decision D-008). Stacks on PRs #45–#48 — earlier commits included until those merge.

Changes

  • schema.py: completion_events gains nullable actor_child_id INTEGER REFERENCES children(id); a second actor-pair coherence CHECK enforces the full shape ('user': actor_user_id set, actor_child_id NULL; 'panel': actor_child_id set, actor_user_id NULL); table body extracted to COMPLETION_EVENTS_TABLE_SQL for the rebuild migration.
  • migrations.py: migration 6 rebuilds legacy-shaped completion_events (CHECKs cannot be added in place), backfilling legacy panel rows to their own child_id via INSERT..SELECT — no UPDATE/DELETE path, so append-only holds; ids preserved; PRAGMA foreign_key_check verified in-transaction before the stamp.
  • dao_instances.py: append() takes actor_child_id; validates both actor shapes and the tapped child's existence atomically under the connection lock.
  • tests: coherence CHECK matrix (5 parametrized bad shapes), unknown actor_child_id FK, panel/user round-trips at DDL and DAO layers, migration-6 backfill on a frozen v2-shape seed (panel row backfills, admin row stays NULL, ids preserved); leak-guard INSERT exemptions for the migration layer documented.

Verification

uv run pytest: 819 passed.

Closes task d77a9756 (feature 14, task 1.5).

Implements IMPLEMENTATION-ORDER.md §1.5 (decision D-008). Stacks on PRs #45–#48 — earlier commits included until those merge. ## Changes - **schema.py**: completion_events gains nullable actor_child_id INTEGER REFERENCES children(id); a second actor-pair coherence CHECK enforces the full shape ('user': actor_user_id set, actor_child_id NULL; 'panel': actor_child_id set, actor_user_id NULL); table body extracted to COMPLETION_EVENTS_TABLE_SQL for the rebuild migration. - **migrations.py**: migration 6 rebuilds legacy-shaped completion_events (CHECKs cannot be added in place), backfilling legacy panel rows to their own child_id via INSERT..SELECT — no UPDATE/DELETE path, so append-only holds; ids preserved; PRAGMA foreign_key_check verified in-transaction before the stamp. - **dao_instances.py**: append() takes actor_child_id; validates both actor shapes and the tapped child's existence atomically under the connection lock. - **tests**: coherence CHECK matrix (5 parametrized bad shapes), unknown actor_child_id FK, panel/user round-trips at DDL and DAO layers, migration-6 backfill on a frozen v2-shape seed (panel row backfills, admin row stays NULL, ids preserved); leak-guard INSERT exemptions for the migration layer documented. ## Verification uv run pytest: 819 passed. Closes task d77a9756 (feature 14, task 1.5).
dev_agent added 5 commits 2026-09-15 10:26:02 -04:00
Per design/IMPLEMENTATION-ORDER.md 1.1 and decision D-007: the domain
model takes the quest vocabulary pre-release. SCHEMA_V1 now creates
quest_definitions/quest_instances directly; DAO classes, records and
error messages follow (QuestDefinitionsDao, QuestInstancesDao). The
migration runner gains migration 2, an async-callable step that detects
surviving task_* tables on dev machines and ALTER TABLE RENAMES them
inside the migration transaction, with FK clauses rewritten by SQLite.
Migration 1's content rewrite is legal because nothing was ever
shipped; dev databases stamped version 1 ride migration 2 up to 2.
Per IMPLEMENTATION-ORDER.md 1.3: a quest_definition_assignees table
(definition_id, child_id) with a composite primary key and FKs to
quest_definitions and children replaces the single child_id column on
quest_definitions.

- schema.py: definitions DDL loses child_id; new assignees table joins
  SCHEMA_V1_STATEMENTS; the definitions table body is extracted to
  QUEST_DEFINITIONS_TABLE_SQL so the rebuild migration cannot drift
  from the canonical shape.
- migrations.py: callable migrations now own their transaction and
  stamp via stamp_schema_version(database, version), because the
  rebuild must bracket itself with PRAGMA foreign_keys OFF/ON. New
  migration 3 creates the assignees table and, when a pre-D-008
  quest_definitions still carries child_id, rebuilds the table via
  SQLite's canonical procedure (create, copy, move assignees, drop,
  rename), verifies PRAGMA foreign_key_check inside the transaction,
  and stamps. Migration 2 converted to the new callable contract.
- dao_rules.py: QuestDefinitionsDao.create drops the child_id argument
  and applies assignee_child_ids atomically; set_assignee is replaced
  by add_assignee (idempotent, active-child validated under the
  connection lock), remove_assignee and list_assignees; list_by_child
  reads through the assignees table.
- dao_instances.py: upsert validates the child against
  quest_definition_assignees instead of the definitions column.
- tests: schema shape tests for the assignees table (columns, PK, FKs,
  duplicate/unknown constraints); DAO tests rewritten for the new
  assignment API; migration tests for the v2 -> v3 rebuild (rows and
  instances survive, child_id gone, foreign_key_check clean).
Per IMPLEMENTATION-ORDER.md 1.4: a quest_definition_windows table
(definition_id, window, due_time) declares which day windows a
definition spans, one row per definition+window via the composite
primary key; window is CHECK-constrained to morning/afternoon/evening.

- const.py: WINDOW_MORNING/AFTERNOON/EVENING, QUEST_WINDOWS canonical
  order, WINDOW_CLOCK_RANGES with the documented boundaries and the
  boundary-to-later-window rule documented for Feature 07.
- schema.py: windows DDL joins SCHEMA_V1_STATEMENTS; due_time is
  caller-validated strict HH:MM (DAO layer), matching the date policy.
- migrations.py: migration 4 is a plain CREATE IF NOT EXISTS statement
  list (no rebuild needed for a new table).
- dao_rules.py: QuestDefinitionsDao gains upsert_window (idempotent
  per window, due_time refresh in place), list_windows (const order),
  remove_window; _validate_time/_validate_window helpers.
- tests: windows DDL shape/constraints (CHECK matrix, duplicate,
  unknown definition), DAO round-trips, const range shapes; leak
  guards extended to the new table.
Per IMPLEMENTATION-ORDER.md 1.2: quest_instances gains a NOT NULL
window column (CHECK-constrained to the const window spellings) and
the table-level UNIQUE constraint widens to the full fan-out tuple.

- schema.py: instance table body extracted to QUEST_INSTANCES_TABLE_SQL
  shared with the rebuild migration; uniqueness docstring rewritten for
  the widened key.
- migrations.py: migration 5 rebuilds a pre-window quest_instances via
  the canonical procedure; migrated rows are pinned to the documented
  default 'morning' window (pre-window rows were whole-day obligations;
  future dates are regenerated with real windows by materialization),
  preserving ids so completion_events keep pointing at the same
  instances; PRAGMA foreign_key_check verified inside the transaction.
- dao_instances.py: upsert takes required keyword window and is
  idempotent on the widened tuple; the immutable-once-completed check
  and the read-back get() are keyed on the full tuple; window spelling
  validated via dao_rules._validate_window.
- tests: schema shape (window column, widened autoindex columns),
  twice-daily and shared-assignee fan-out cases at both the DDL and DAO
  layers (incl. idempotent re-runs), cross-child same-key allowance,
  window CHECK/NOT NULL constraints, v2 seed frozen to the true
  version-2 shapes and its migration assertions widened.
Per IMPLEMENTATION-ORDER.md 1.5: a nullable actor_child_id column
records which panel profile was tapped; admin events stay NULL.

- schema.py: completion_events table body extracted to
  COMPLETION_EVENTS_TABLE_SQL; the new column carries a FK to
  children(id) and the actor-pair coherence is extended to a second
  CHECK: 'user' events require actor_user_id and forbid
  actor_child_id, 'panel' events require actor_child_id and forbid
  actor_user_id.
- migrations.py: migration 6 rebuilds legacy-shaped completion_events
  via the canonical procedure (CHECKs cannot be added to an existing
  table), backfilling each legacy panel row to its own child_id
  (pre-D-008 panel completions were always the tapped child) via
  INSERT..SELECT — no UPDATE or DELETE path exists, so the append-only
  guarantee is untouched; ids preserved; PRAGMA foreign_key_check
  verified in-transaction.
- dao_instances.py: append() takes actor_child_id, validates both
  actor shapes (panel requires it, user forbids it) and the tapped
  child's existence under the connection lock; the record and column
  list grow accordingly.
- tests: coherence CHECK matrix (5 bad shapes), unknown
  actor_child_id FK, panel/user round-trips at DDL and DAO layers,
  migration-6 backfill test on a frozen v2-shape seed; the
  completion_events leak guard gains justified INSERT exemptions for
  the migration layer (UPDATE/DELETE stay forbidden everywhere).
dev_agent merged commit 82d1be97fa into feature/c16267ae-75be-4a4e-b14f-e41c06c8ab11-design-model-changes 2026-09-15 10:35:47 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Maniacs_Mansion/NestQuest#49