Repository navigation
Raise the grace ceiling to 30 days and the overdue lookback to 31 - #94
Conversation
14 was the largest grace a 15-day lookback could honour, and it was too low: the demo catalog's annual "Contractor induction refresh" was authored with 21 days of grace — an ordinary configuration for an annual duty — and #82 had to cut it to 14 to fit. A limit a plausible first configuration trips is a limit in the wrong place, so the lookback moves instead: OVERDUE_LOOKBACK_DAYS 31, grace_days max 30 on duly_duty and on duly_catalog_item, and the demo row goes back to the 21 its author meant. The coupling is unchanged in kind, only in value. lookback >= max_grace + 1 is now stated as an invariant at each of the three sites, naming the other two and the file they live in, because the three numbers are hand-maintained in files that cannot see each other and only test/reminders.test.ts holds them together — which is exactly how the original 21-against-15 mismatch stayed invisible. Measured rather than inferred, by driving the real time-relative sweep and the real flow against a booted kernel: a duty with grace 30 whose task is 31 days past due is swept and escalated, emitting one notification to the owner on topic duly.task_overdue; the same record is not even swept under the old 15-day window; a sibling task 30 days past due is swept but not notified, so the gate is still day equality. 31 is refused at write time on both objects as max_value, "Grace (days) must be ≤ 30". Closes #89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Reviewed — merging. The verification is what I asked for and better than I asked for.I said: show the escalation firing at
And the demo seed now logs zero Building the rig as a throwaway and not committing it was the right instinct: it proves a runtime fact for this change, and as a permanent test it would be a slow kernel boot pinning platform behaviour from outside. The comments are the part that will still matter in six months. Three sites, hand-maintained, in files that cannot see each other, coupled only by a test — that is exactly the configuration that let the original 15-vs-21 mismatch sit unnoticed. Each site now naming the invariant ( Gates, re-run by me on the head merged with current Generated by Claude Code |
Fixes #89
Implements the triage decision on the card: no, 14 is not the product answer —
grace_daysmax goes to 30 andOVERDUE_LOOKBACK_DAYSto 31, the two stay coupled, and the demo catalog'sContractor induction refreshgoes back to thegraceDays: 21its author wrote.What changed
src/flows/reminders.flow.ts·OVERDUE_LOOKBACK_DAYSsrc/objects/duty.object.ts·grace_days.maxsrc/objects/catalog-item.object.ts·grace_days.max(mirrored)src/data/demo-catalog.ts·Contractor induction refreshgraceDays: 14graceDays: 21test/reminders.test.ts·withinDays-15-31test/cadence-number-constraints.test.ts·BOUNDS.grace_daysmax: 14max: 30The coupling test is unchanged in kind — it still derives the lookback from the flow, reads
maxoff both objects and assertsgraceMax === lookback - 1, so it fails if any one of the three moves alone. Only the values it holds together moved.The shape, which the card said matters more than the number
The three numbers are hand-maintained in files that cannot see each other, and only a test couples them — which is exactly why the original 21-against-15 mismatch stayed invisible until #82 went looking. Each site now states the invariant
lookback >= max_grace + 1, names the other two sites and their file paths, and says what breaks in each direction:duly_catalog_itemis the same hole reached one object earlier, becauseapplyCatalogHandlercopies the field onto every duty it creates — a ceiling that disagrees between the two objects is an apply that refuses partway through a fan-out.The cost of the longer window is written down where the number lives, in the terms the triage used: what scales is the still-open tasks inside the window (the sweep filter is
LIVE_TASK), not the task count, because a task leaves that set the moment it is completed.Verified, not inferred
Both edges were measured against a booted kernel driving the real
TimeRelativeTriggersweep and the real flow, not derived from the arithmetic. The rig mountedAutomationServicePlugin, registered a capturingmessagingservice (the service thenotifynode emits through), inserted a duty withgrace_days: 30and a task 31 days past due, and ran one sweep at the real clock.1 · A 30-day grace really is escalated. The engine's own run summary for the day-one task:
acted=1, and the notification that came out of it:{ "topic": "duly.task_overdue", "audience": ["u_grace30"], "severity": "warning", "payload": { "title": "Induction refresh — day one past grace 30", "body": "Past due since 2026-08-01." }, "source": { "object": "duly_task", "id": "duly_task-…" } }2 · The control that makes it falsifiable. The identical record under the old
-15window:It is not merely un-notified — it never enters the sweep at all, which is the silent inertness the card is about, and it is the lookback move that fixes it.
3 · The gate is still day equality, not "anything in the window". A sibling task 30 days past due on the same duty was swept in the same run and did not notify:
4 · 31 is still refused at write time, on both objects, with the
max_valueenvelope #82's tests assert:Measured on
duly_dutyand onduly_catalog_item;grace_days: 30and the demo's21are both accepted on the catalog item.5 · The demo seed writes the 21 row cleanly.
test/seed.test.tsopts the demo seed in and drives the realSeedLoaderService; the full run logs zeroFailed to writelines, where the pre-#89 ceiling produced[SeedLoader] Failed to write duly_catalog_item record #11 … Grace (days) must be ≤ 14.The rig itself is deliberately not committed. Mounting the trigger and automation plugins by hand inside a test asserts the test's wiring rather than the app's — the false green
test/dispatch-wiring.test.tsandtest/trigger-capability.test.tsare both written to avoid. The durable guard for this change is the coupling assertion intest/reminders.test.ts, which is where it already lived.Gates
All four green on
8ddef76, the commit at the head of this branch, run after the final commit:The one
validatewarning is the documented expected state of this checkout —hierarchy-securityis provided by@objectstack/security-enterprise, deliberately not installed (AGENTS.md rule 7). No changeset: this repo has no changeset mechanism.Generated by Claude Code