Regenerate future instances on definition change (Feature 07, task 4) (#78)

This commit was merged in pull request #78.
This commit is contained in:
2026-09-17 14:24:22 -04:00
parent 168bb7b2f1
commit 2146531665
4 changed files with 208 additions and 43 deletions
+34 -1
View File
@@ -42,6 +42,7 @@ from __future__ import annotations
import datetime
from .const import DEFAULT_HORIZON_DAYS
from .dao_instances import QuestInstancesDao
from .dao_rules import (
_validate_date,
@@ -166,4 +167,36 @@ async def materialize(
if written is not None:
count += 1
cursor += datetime.timedelta(days=1)
return count
return count
async def regenerate_for_definition(
database: NestQuestDatabase,
definition_id: int,
) -> int:
"""Regenerate a definition's future instances after a config change.
The Feature 06 edit/assignment/activation paths call this after a
successful write so future instances track the new state: the
definition's open instances at or after today are deleted (never a
completed instance, never the past) via
:meth:`~.dao_instances.QuestInstancesDao.delete_future_uncompleted`,
then the materialization walk re-runs over the rolling horizon
``[today, today + DEFAULT_HORIZON_DAYS]`` so the (changed) rule,
assignee set and windows re-materialize against today's state.
"today" is computed the same way the walk computes it —
``datetime.date.today()`` — so the delete cutoff and the
re-materialization range share one anchor, and the walk's
``upsert_if_valid`` no-past guard never rejects the regenerated
range. Returns the number of instances the re-materialization
upserted across the whole snapshot (idempotent — other active
definitions' tuples refresh in place, never duplicate).
"""
today = datetime.date.today()
start_date = today.isoformat()
end_date = (today + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)).isoformat()
instances = QuestInstancesDao(database)
await instances.delete_future_uncompleted(definition_id, start_date)
return await materialize(database, start_date, end_date)
@@ -58,6 +58,7 @@ from .dao_rules import (
schedule_rule_to_storage,
)
from .db import NestQuestDatabase
from .materialize import regenerate_for_definition
from .recurrence import ScheduleRule, occurs_on
@@ -341,6 +342,8 @@ async def edit_quest_definition(
schedule_rule_storage_from_record(snapshot.rule)
)
await regenerate_for_definition(database, definition_id)
return CreatedQuestDefinition(
definition=snapshot.definition,
rule=decoded_rule,
@@ -371,7 +374,7 @@ async def assign_child(
_validate_child_id(child_id)
dao = QuestDefinitionsDao(database)
try:
return await dao.add_assignee(definition_id, child_id)
assignees = await dao.add_assignee(definition_id, child_id)
except ValueError as error:
# The DAO names only the child/definition; re-raise so the
# public contract names the field the caller actually passed.
@@ -381,6 +384,8 @@ async def assign_child(
if message.startswith("quest definition "):
raise ValueError(f"definition_id: {message}") from error
raise
await regenerate_for_definition(database, definition_id)
return assignees
async def unassign_child(
@@ -405,7 +410,9 @@ async def unassign_child(
raise ValueError(
f"definition_id: quest definition {definition_id} does not exist"
)
return await dao.remove_assignee(definition_id, child_id)
removed = await dao.remove_assignee(definition_id, child_id)
await regenerate_for_definition(database, definition_id)
return removed
async def set_quest_definition_active(
@@ -457,6 +464,7 @@ async def set_quest_definition_active(
rule = schedule_rule_from_storage(
schedule_rule_storage_from_record(rule_record)
)
await regenerate_for_definition(database, definition_id)
return CreatedQuestDefinition(
definition=updated,
rule=rule,