Business-layer functions over ChildrenDao for creating and editing
child profiles, so the Feature 09 service gate never touches the DAO
directly.
- children.py: create_child (display name required and trimmed,
whitespace-only rejected; duplicate display name allowed but logged
as a warning, case-insensitive and whitespace-normalised on both
sides), edit_child (_UNSET sentinel for leave-alone; returns the
updated record; renaming onto a different child's name warns, own
name does not), set_child_active (the only removal path), and
list_children (display order). Explicit None for optional fields
raises instead of silently no-oping: the DAO's update deliberately
writes only provided fields and cannot express NULL, so a clear
request must not masquerade as one. UTC second-precision stamps per
the timestamp policy.
- tests/test_children_registry.py: 27 tests covering the typed
round-trips, name validation matrix, duplicate-name warning
behaviour (create and rename, including the rename-to-self
non-warning), partial edits, refusal of unsupported clears,
deactivation round-trip and list ordering.
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).
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.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.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 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.