diff --git a/stacklets/docs/bot/git_mirror.py b/stacklets/docs/bot/git_mirror.py index 2bc03224..505dd5c5 100644 --- a/stacklets/docs/bot/git_mirror.py +++ b/stacklets/docs/bot/git_mirror.py @@ -53,6 +53,7 @@ from stack.forgejo import ForgejoClient, ForgejoError +from diary_room import household_now from vault_entry import ( slug, capture_hash, @@ -529,6 +530,7 @@ async def _fold_action_items_into_list( author_name: str, author_email: str, previous_path: str = "", + today: str = "", ) -> bool: """Put a record's action items on its topic's list, `lists/.md`. @@ -541,10 +543,15 @@ async def _fold_action_items_into_list( have ticked, reworded or removed them since. When a refile renamed the record (`previous_path`), those items are linked to its new path instead. An item whose text is already on the list is left - alone too. + alone too. An item due before `today` (the household's date when + not given) stays off the list: an old letter filed late, or a + refile of last year's bill, has nothing left to do. The record's + summary still names it. """ source = Link(record_title, record_path) if record_path else None - items = [i for ai in action_items or [] if (i := action_item(ai, source=source))] + today = today or household_now().date().isoformat() + items = [i for ai in action_items or [] if (i := action_item(ai, source=source)) + and (not i.due or i.due >= today)] if not (topic and items): return False path = list_path(topic, bucket=self.shared_bucket) @@ -589,6 +596,7 @@ async def _fold_document_todos( record_path: str, record_title: str, previous_path: str = "", + today: str = "", ) -> None: topics = classification.get("topics") or classification.get("topic") or [] if isinstance(topics, str): @@ -607,6 +615,7 @@ async def _fold_document_todos( message=f"chore(lists): archivist added action items to {self._slug(topic)}", author_name=BOT_USERNAME, author_email=BOT_EMAIL, + today=today, ) async def _fold_capture_todos( @@ -618,6 +627,7 @@ async def _fold_capture_todos( author_name: str, author_email: str, previous_path: str = "", + today: str = "", ) -> None: topic = self._topic_of_capture(target_path) await self._fold_action_items_into_list( @@ -631,6 +641,7 @@ async def _fold_capture_todos( message=f"chore(lists): {author_name} added action items to {topic}", author_name=author_name, author_email=author_email, + today=today, ) # ── Publish ────────────────────────────────────────────────────────── diff --git a/stacklets/docs/bot/pipeline.py b/stacklets/docs/bot/pipeline.py index a73c498f..2aed6bec 100644 --- a/stacklets/docs/bot/pipeline.py +++ b/stacklets/docs/bot/pipeline.py @@ -1155,7 +1155,7 @@ def _build_classify_prompt(*, ocr_text: str, person_names: list[str], - correspondent_aliases: only when the printed sender name on THIS document differs from your canonical answer. Single-element list is fine. - correspondent_facts: stable across documents from the same sender. Address and customer numbers belong here; this month's total does not. - facts: concrete numbers, dates, account numbers, amounts that describe THIS document. When the document itemises purchases or services, each line item is its own fact bullet alongside the top-level totals. Empty list if none. -- action_items: deadlines, payments due, forms to return. Empty list if none. +- action_items: manual family tasks only (pay by transfer, sign/return form, book, cancel by date). NOT: direct debit, SEPA mandate, card on file, standing order, info or confirmation only. [] if none. Document text: --- diff --git a/tests/unit/stacklets/test_git_mirror.py b/tests/unit/stacklets/test_git_mirror.py index c4a1f224..cb14ddef 100644 --- a/tests/unit/stacklets/test_git_mirror.py +++ b/tests/unit/stacklets/test_git_mirror.py @@ -468,6 +468,7 @@ async def test_a_document_starts_the_topics_list(self, mirror): }, record_path="family/documents/2026/03/2026-03-02-campsite-p12.md", record_title="Campsite booking", + today="2026-03-02", ) written = client.files["family/lists/camping.md"]["content"] @@ -511,18 +512,54 @@ async def test_a_refiled_record_adds_nothing_and_its_items_follow_a_rename(self, await mirror._fold_document_todos( client, classification={"topics": ["Education"], "action_items": [{"action": "Sign the slip", "due": "2026-10-23"}]}, - record_path=old, record_title="Permission slip") + record_path=old, record_title="Permission slip", today="2026-10-09") await mirror._fold_document_todos( client, classification={"topics": ["Education"], "action_items": [{"action": "Sign and return the slip", "due": "2026-10-23"}]}, - record_path=new, record_title="School notice", previous_path=old) + record_path=new, record_title="School notice", previous_path=old, today="2026-10-09") written = client.files["family/lists/education.md"]["content"] assert "Sign and return" not in written assert ("- [ ] Sign the slip [Permission slip]" "(../documents/2026/10/2026-10-09-school-notice-p72.md) 📅 2026-10-23") in written + @pytest.mark.asyncio + async def test_an_item_already_past_its_due_date_stays_off_the_list(self, mirror): + """An old letter filed late, or last year's bill refiled, has nothing left to do. + The record's summary still names the item; only the list leaves it out.""" + client = _FakeTodoClient() + await mirror._fold_document_todos( + client, + classification={"topics": ["Fahrzeug"], "action_items": [ + {"action": "Rechnung überweisen", "due": "2026-08-14"}, + {"action": "Unterlagen abholen", "due": "2026-10-13"}, + "Reifen einlagern", + ]}, + record_path="family/documents/2026/08/werkstatt-p40.md", + record_title="Werkstatt Rechnung", + today="2026-10-13", + ) + + written = client.files["family/lists/fahrzeug.md"]["content"] + assert "Rechnung überweisen" not in written + assert "Unterlagen abholen" in written, "due today is still ahead of the family" + assert "Reifen einlagern" in written, "an item without a date never runs out" + + @pytest.mark.asyncio + async def test_a_record_with_only_past_items_starts_no_list(self, mirror): + client = _FakeTodoClient() + await mirror._fold_document_todos( + client, + classification={"topics": ["Fahrzeug"], + "action_items": [{"action": "Rechnung überweisen", "due": "2026-08-14"}]}, + record_path="family/documents/2026/08/werkstatt-p40.md", + record_title="Werkstatt Rechnung", + today="2026-10-13", + ) + + assert client.puts == [] + @pytest.mark.asyncio async def test_a_new_list_is_titled_with_the_topics_name(self, mirror): client = _FakeTodoClient()