From 575afcf3df6c9a33a7795971e814f6dc766e068f Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 13 Oct 2026 18:59:13 +0200 Subject: [PATCH 1/2] fix(archivist): stop treating a direct debit as an action item A bill paid by direct debit came back with an action item such as "Rechnungsbetrag per Lastschrift begleichen", and the archivist put it on the topic's list, where nobody had anything to do. The classifier was asked for "payments due", which covers money the sender collects on its own. Action items are now only what someone in the family has to do by hand. A direct debit, a card on file or a standing order is not one, and neither is anything the document only reports or confirms. --- stacklets/docs/bot/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: --- From 1f1210e795aa19ec55952df5f2e7106cd84aadff Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 13 Oct 2026 19:01:52 +0200 Subject: [PATCH 2/2] fix(archivist): keep action items already past due off the lists A document filed after its deadline, or last year's bill refiled, put its action items on the topic's list with a due date that had already passed. The family then saw months-old to-dos nobody would ever do. An item due before the household's date now stays off the list. Items due today or later, and items without a date, still go on. The record's summary keeps every item. --- stacklets/docs/bot/git_mirror.py | 15 +++++++-- tests/unit/stacklets/test_git_mirror.py | 41 +++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) 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/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()