diff --git a/owner/.changes/260727-1014.md b/owner/.changes/260727-1014.md new file mode 100644 index 0000000..1e3009d --- /dev/null +++ b/owner/.changes/260727-1014.md @@ -0,0 +1,5 @@ +--- +bump: patch +section: Changed +--- +`deriveReminders`: pending-вузол дає `personal_today` лише коли дедлайн сьогодні чи прострочений, а не для будь-якого дедлайну в 24-годинному горизонті — pending-вузол з дедлайном "завтра" тепер коректно потрапляє в `deadline_due`. diff --git a/owner/src/docs/reminders.md b/owner/src/docs/reminders.md index e0a4b61..66fb35d 100644 --- a/owner/src/docs/reminders.md +++ b/owner/src/docs/reminders.md @@ -3,36 +3,44 @@ type: JS Module title: reminders.js resource: owner/src/reminders.js docgen: - crc: b53801dc + crc: ccc95f23 model: openai-codex/gpt-5.4-mini tier: cloud-min score: 100 - issues: judge:inaccurate:0.98 + issues: judge-refine:kept-original,judge:inaccurate:0.98 judgeModel: openai-codex/gpt-5.4-mini --- ## Огляд -`deriveReminders`, `applySnoozes`, `bucketPersonal` і `reminderRibbon` разом формують особистий список нагадувань із deadline, застарілих ескалацій і персональних snooze. `ESCALATION_STALE_DAYS` визначає, коли ескалація вважається застарілою, а `nextMidnight` задає межу для добового оновлення. Це потрібно, щоб відокремити особисті нагадування від решти задач і показати їх у компактному вигляді через `reminderRibbon`. +Файл об’єднує нагадування з різних джерел через `deriveReminders`, застосовує `applySnoozes` до відкладених елементів і окремо виділяє особисті задачі в `bucketPersonal`, щоб сформувати єдину стрічку актуальних сповіщень. `reminderRibbon` збирає цей результат у компактне подання для показу на поточний момент, а `nextMidnight` і `ESCALATION_STALE_DAYS` задають межі оновлення та актуальності для прострочених ескалацій. ## Поведінка -- `ESCALATION_STALE_DAYS` — задає поріг у днях, після якого відкрита ескалація на мою адресу вважається застарілою. -- `nextMidnight` — повертає момент наступної локальної півночі як стандартний час для відкладеного нагадування. -- `deriveReminders` — збирає активні нагадування з workspace-лісу за правилами deadline та застарілої ескалації, відсікаючи не-мій scope. -- `applySnoozes` — прибирає з переліку нагадувань ті, для яких ще діє персональний snooze. -- `bucketPersonal` — групує особисті задачі брифу в кошики дедлайнів від найтерміновішого до пізнішого. -- `reminderRibbon` — формує коротку стрічку-лічильник для режиму «Рішення» на основі активних нагадувань. +ESCALATION_STALE_DAYS задає спільний поріг «застарілості» для ескалацій і визначає, коли відкритий кейс уже вважається достатньо старим, щоб потрапити в нагадування. + +nextMidnight задає дефолтний горизонт snooze «до завтра»: нагадування, відкладене до цієї мітки, залишається тихим до настання наступної локальної доби. + +deriveReminders збирає нагадування з дерева воркспейсів і пов’язаного контексту, поєднуючи особисті дедлайни, прострочені задачі та ескалації в один список; результат уже впорядкований так, щоб найтерміновіші були першими. Дані для нього приходять із лісу вузлів, scope-класифікації та, за наявності, серій ескалацій і ідентифікатора користувача. + +applySnoozes відсіює вже заглушені нагадування за активними snooze-мітками й залишає лише ті, що мають звучати зараз; це проміжний фільтр між зібраними нагадуваннями та відображенням користувачу. + +bucketPersonal групує особисті задачі в кошики за терміновістю і зберігає фіксований порядок від найпекучішого до менш термінового, опускаючи порожні групи. Вона працює з уже зібраними рядками, тож не змінює джерело даних, а лише перекладає його в форму для брифу. + +reminderRibbon будує тонку зведену стрічку поверх активних нагадувань після applySnoozes: показує лише лічильники й заголовок, не впливаючи на вибір режиму. Якщо активних нагадувань немає, стрічка не показується. ## Публічний API -- ESCALATION_STALE_DAYS — поріг давності, після якого ескалація вважається протермінованою -- nextMidnight — ставить дефолтне відкладення до наступної локальної півночі -- deriveReminders — витягає нагадування мого скоупу за трьома правилами спеки -- applySnoozes — ховає нагадування, які ще не вийшли з snooze -- bucketPersonal — розкладає особисті задачі брифу по кошиках дедлайнів від найтерміновішого; порожні не показує -- reminderRibbon — показує в режимі «Рішення» компактну стрічку лічильників нагадувань, не впливаючи на вибір режиму +- nextMidnight — Північ наступної локальної доби — дефолтний горизонт snooze +(«тихо до завтра»). +- deriveReminders — Збирає нагадування мого скоупу за трьома правилами спеки. +- applySnoozes — Відсіює заглушені нагадування: snooze діє, доки його until у майбутньому. +- bucketPersonal — Групує особисті задачі брифу в кошики дедлайнів (порожні кошики +опускаються, порядок фіксований — від найпекучішого). +- reminderRibbon — Тонка стрічка нагадувань для режиму «Рішення»: лічильники без впливу +на вибір режиму (правило режимів недоторканне — конституція 260714). +- ESCALATION_STALE_DAYS — визначає, через скільки днів прострочене персональне нагадування переходить у стан escalation ## Гарантії поведінки -- Read-only: не виконує операцій запису (ФС/БД). +- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати. diff --git a/owner/src/reminders.js b/owner/src/reminders.js index 21789ef..b479c3c 100644 --- a/owner/src/reminders.js +++ b/owner/src/reminders.js @@ -51,8 +51,8 @@ function walk(nodes, pick) { /** * Нагадування одного вузла за правилами deadline_due / personal_today. - * Правила зі спеки перетинаються на pending (deadline сьогодні ⊂ горизонт - * 24 год), тож вузол дає щонайбільше одне нагадування: pending — + * Правила зі спеки перетинаються на pending із дедлайном сьогодні або в + * минулому, тож вузол дає щонайбільше одне нагадування: такий pending — * personal_today, решта нерозвʼязаних — deadline_due. * @param {object} node вузол зі scan * @param {{ label: string, path: string }} workspace воркспейс вузла @@ -64,8 +64,10 @@ function deadlineReminder(node, workspace, now) { const deadline = Date.parse(node.deadline) if (Number.isNaN(deadline) || deadline > now + DUE_HORIZON_MS) return null const overdue = deadline < now - const rule = node.state === 'pending' ? 'personal_today' : 'deadline_due' + const isPendingToday = node.state === 'pending' && deadline < dayStart(now) + DAY_MS + const rule = isPendingToday ? 'personal_today' : 'deadline_due' const what = rule === 'personal_today' ? 'твоя задача' : 'гілка без результату' + const timing = rule === 'personal_today' ? 'сьогодні' : 'наближається' return { id: `${rule}|${workspace.path}|${node.path}|${node.deadline}`, rule, @@ -76,7 +78,7 @@ function deadlineReminder(node, workspace, now) { stake: overdue ? 0 : 1, headline: overdue ? `дедлайн ${node.deadline} минув — ${what} прострочена` - : `дедлайн ${node.deadline} сьогодні — ${what}` + : `дедлайн ${node.deadline} ${timing} — ${what}` } } diff --git a/owner/src/tests/reminders.test.js b/owner/src/tests/reminders.test.js index 412dd9a..f96cceb 100644 --- a/owner/src/tests/reminders.test.js +++ b/owner/src/tests/reminders.test.js @@ -39,14 +39,19 @@ describe('deriveReminders', () => { const rows = deriveReminders({ ...forestOf([ { path: 'mine', state: 'pending', deadline: '2026-07-17T17:00:00', children: [] }, - { path: 'stale', state: 'pending', deadline: '2026-07-15T09:00:00', children: [] } + { path: 'stale', state: 'pending', deadline: '2026-07-15T09:00:00', children: [] }, + { path: 'tomorrow', state: 'pending', deadline: '2026-07-18T10:00:00', children: [] } ]), now: NOW }) expect(rows.map(r => [r.path, r.rule, r.overdue])).toEqual([ ['stale', 'personal_today', true], - ['mine', 'personal_today', false] + ['mine', 'personal_today', false], + ['tomorrow', 'deadline_due', false] ]) + expect(rows.find(row => row.path === 'tomorrow')?.headline).toBe( + 'дедлайн 2026-07-18T10:00:00 наближається — гілка без результату' + ) }) it('чужі й межові вузли нагадувань не породжують — лише мій скоуп', () => {