/** * cadence.ts, when an unresolved thing speaks. * * The governing principle is that nothing unresolved is ever DROPPED. It is * worth stating what that does and does not mean, because the difference cost * the owner a day of being told about their own birthday every hour: * * - It means the OPEN ITEM survives. It stays in the store, it stays * enumerable, and asking "anything coming up?" finds it. Silence from the * owner never deletes it. * - It does NOT mean the push repeats. An unanswered thing is not a thing to * say again, and again, and again. It was read that way, and on an hourly * sweep "raise anything whose due date has arrived" plus "a due date that * cannot move past the occurrence" is an unbounded loop wearing the costume * of a policy. * * So a nudge speaks at TWO NAMED MOMENTS and no others: the day it enters its * lead window, and the day itself. Each is recorded as served the moment it is * used, and a served boundary is never served twice. The ceiling is therefore a * property of the record rather than a rule someone has to remember to apply, * there is no counter to overflow, no timestamp to compare, and no arrangement * of restarts, clock changes or sweep intervals that produces a third push. * * Conflicts and interviews keep the older repeating rhythm, and deliberately: a * conflict is a fact about the owner's FILE that stays wrong until they fix * it, and an interview is a conversation the owner walked out of * mid-sentence. Neither is a countdown to a date, and neither was the thing * drowning them. * * Everything in this file is pure. It takes calendar days and record state and * gives back calendar days and record state; it never reads a clock, so the * sweep's behaviour is reproducible from its inputs alone. */ import { type IsoDate } from './dates.js'; import { type OpenItem, type OpenItemKind, type Plan, type RaiseBoundary } from './types.js'; /** The two knobs the nudge rhythm is built from. */ export interface CadencePolicy { /** Ordinary gap between nudges, in days. Default 3. */ readonly cadenceDays: number; } export declare function nudgeItemId(occasionId: string, occurrence: IsoDate): string; /** * A conflict's id carries no occurrence. * * Two declared dates disagreeing is not a fact about this year's birthday, it is * a fact about the record, and it stays open until the owner fixes the file, across * however many occurrences pass in the meantime. */ export declare function conflictItemId(occasionId: string): string; export declare function interviewItemId(occasionId: string, occurrence: IsoDate): string; /** * The day the `day-of` boundary actually falls on. * * The occurrence, unless the owner will be AWAY on it and the away adjustment * is on, in which case it moves to the day before they leave. Design rule: * being away modifies the one boundary it can apply to: a reminder that * arrives while the owner is abroad has already missed the window it existed * to protect, and there is nothing useful about being told on the day when * the useful day was before the flight. * * The `lead` boundary is not adjustable, it is the top of the window, and * there is nothing earlier to move it to. */ export declare function dayOfBoundaryDate(occurrence: IsoDate, today: IsoDate, plans: readonly Plan[], awayAdjust: boolean): IsoDate; /** * Which boundary TODAY is, for an occurrence already inside its lead window. * * Two moments, so two answers. On or after the day-of date it is `day-of`; * anywhere else inside the window it is `lead`. The caller has already * established that the window is open, this does not decide whether to speak, * only which of the two moments a decision to speak would be spending. */ export declare function boundaryOn(today: IsoDate, dayOfDate: IsoDate): RaiseBoundary; /** True when this item has already spoken at that boundary. */ export declare function hasServed(item: OpenItem, boundary: RaiseBoundary): boolean; /** True when both boundaries are spent and this item can never push again. */ export declare function isSpent(item: OpenItem): boolean; /** * When a "later" comes back. * * "Not yet" three weeks out is not a decline, and returning it the next morning * would make "later" mean nothing. It comes back roughly halfway to the date, * far enough that it reads as having been listened to, near enough that there is * still time to order something. Never sooner than tomorrow, never past the day * itself. */ export declare function laterReturnDate(today: IsoDate, occurrence: IsoDate): IsoDate; /** * A dropped interview resumes the next day, and never after the date itself. * * The owner was mid-thread, so this is a live conversation they walked away * from rather than a question they have not engaged with. One day is the * shortest gap that is not badgering, and it was my call, the plan says * "nudge again later and resume" without naming an interval. * * The clamp lives HERE rather than at the call site. The sweep used to add a day * and clamp inline, which made this function a second, unused definition of the * same rule sitting next to the real one, the drift class every other comment * in this module argues against, and the kind that survives review because both * copies are correct on the day they are written. */ export declare function interviewResumeDate(today: IsoDate, occurrence?: IsoDate): IsoDate; /** * Move a nudge that would land while the owner is away. * * Design rule: being somewhere is trackable state and may modify nudge times. * The useful modification is EARLIER, not later: the owner cannot have * something delivered to a house they are not in, so a reminder that arrives * while they are abroad has already missed the window it existed to protect. * So a nudge due inside an away window moves to the day before they leave. * * When the owner has ALREADY left, the window started before today, there is * nothing earlier to move to, and the nudge stands. Holding it until they are * back would be the system quietly deciding a loved one's birthday can wait. */ export declare function adjustForAway(due: IsoDate, today: IsoDate, plans: readonly Plan[]): IsoDate; /** True when an open item may be raised on `today`. */ export declare function isDue(item: OpenItem, today: IsoDate): boolean; /** Build the open item a first raise creates. */ export declare function openItemFor(input: { readonly kind: OpenItemKind; readonly id: string; readonly occasionId: string; readonly occurrence: IsoDate; readonly now: number; readonly dueOn: IsoDate; readonly expiresAfter?: IsoDate | undefined; /** The boundary this first raise is spending. Nudges only. */ readonly boundary?: RaiseBoundary | undefined; }): OpenItem; /** * The same item, raised once more. Conflicts and interviews only. * * Takes no boundary because neither of those has one: they repeat on a date, as * they always did. A nudge goes through {@link raisedAtBoundary} instead, which * is the only way a nudge's raise count ever moves. */ export declare function raisedAgain(item: OpenItem, now: number, dueOn: IsoDate): OpenItem; /** * The same nudge, having now spoken at one boundary, and spent it. * * The boundary is added to the served list in the SAME write that increments * the count, so there is no window in which a raise happened but was not * recorded as spending its boundary. Adding a boundary already present is a * no-op rather than an error: the caller should not have asked, and duplicating * it would be the one way to smuggle a third push past the ceiling. */ export declare function raisedAtBoundary(item: OpenItem, now: number, dueOn: IsoDate, boundary: RaiseBoundary): OpenItem; /** * Rebuild the raise ledger of an item written before boundaries existed. * * A machine that has been running the old repeating cadence holds nudge items * with a raise count and no served boundaries, the owner's own birthday, at * the time this was written, sat at five raises and climbing. Those items are * not deleted and not resolved: nothing about them was resolved, and dropping * them would trade one broken promise for another. They are marked as having * already spoken, and they go quiet. * * The mapping is the conservative reading of what already happened to the owner: * * - one raise → the lead boundary is spent, the day itself is still owed. * They were told once, at the top of the window, which is exactly what the * new rule would have done. * - two or more → both are spent. They have already heard about this * occurrence at least as often as the ceiling allows, and the honest * correction is silence, not one more. * * Returns `null` when there is nothing to reconcile, so the caller can count * and receipt only the items it actually changed. */ export declare function reconcileRaiseLedger(item: OpenItem): OpenItem | null; //# sourceMappingURL=cadence.d.ts.map