import { type IsoDate } from './dates.js'; import { type GiftRecord, type Interview, type OccasionAcknowledgement, type OccasionMirrorRecord, type OccasionStateDisclosure, type OccasionSweepReport, type OpenItem } from './types.js'; /** Caps. Generous against any real life, finite against an unbounded file. */ export declare const MAX_ACKNOWLEDGEMENTS = 2000; export declare const MAX_GIFT_RECORDS = 2000; export declare const MAX_OPEN_ITEMS = 500; export declare const MAX_INTERVIEWS = 200; export declare const MAX_MIRRORS = 2000; interface OccasionStateSnapshot extends Record { version: 1; acknowledgements: OccasionAcknowledgement[]; gifts: GiftRecord[]; openItems: OpenItem[]; interviews: Interview[]; mirrors: OccasionMirrorRecord[]; lastSweep: OccasionSweepReport | null; } /** * Rebuild a snapshot record by record. * * Nothing throws. A file that is JSON but holds the wrong shape yields whatever * of it was well formed, and the count of what was not is logged, so a * half-corrupt file costs the owner the corrupt half, not the whole history. */ export declare function validateOccasionState(raw: unknown): { snapshot: OccasionStateSnapshot; dropped: number; reconciled: number; }; /** What a sweep needs to know from outside: which occasions still exist. */ export interface OccasionSweepInput { readonly today: IsoDate; readonly now: number; /** Ids still declared in the profile. State for anything else is orphaned. */ readonly declaredOccasionIds: ReadonlySet; /** How long gift history is kept, in years. */ readonly giftHistoryYears: number; } export declare class OccasionStateStore { private readonly filePath; private readonly store; private snapshot; private corruption; /** How many open nudges had their raise ledger rebuilt at load. Disclosed. */ private reconciledOpenItems; /** Whole-file writes run one at a time, in call order. See StoreWriteQueue. */ private readonly writes; constructor(filePath: string); get path(): string; /** * Read the file once, discarding what it cannot understand. * * `loadOrDiscard` rather than `load`: this store's owner has a rule for a torn * record, drop it, record the fact, disclose it, and a store that only threw * would make every later call fail forever over one unreadable byte, * INCLUDING the disclosure call that exists to explain exactly that state. */ private state; private persist; /** The answer recorded for one occurrence, if there is one. */ answerFor(occasionId: string, occurrence: IsoDate): Promise; acknowledgements(): Promise; /** * Record an answer, replacing any earlier answer for the SAME occurrence. * * Replacing rather than appending is the point: "later" then "no" is one * decision that changed, not two decisions, and keeping both would leave the * sweep reading whichever it found first. */ recordAnswer(entry: OccasionAcknowledgement): Promise; /** What the owner landed on for this occasion before, newest first. */ giftHistory(occasionId: string): Promise; recordGift(entry: GiftRecord): Promise; openItems(): Promise; openItem(id: string): Promise; /** Create or replace one open item, addressed by its id. */ putOpenItem(item: OpenItem): Promise; /** * Remove an open item because it is RESOLVED. * * No tombstone and no resolved flag: a resolved item is gone, matching the * profile's own delete-means-delete rule. What survives resolution is the * thing that answers "what happened", the acknowledgement, or the gift * record, not a husk of the question. */ resolveOpenItem(id: string): Promise; interview(id: string): Promise; /** The unfinished interview for one occurrence, if the owner walked away from one. */ activeInterview(occasionId: string, occurrence: IsoDate): Promise; putInterview(interview: Interview): Promise; /** The mirror already written for this occurrence, if there is one. */ mirrorFor(occasionId: string, occurrence: IsoDate): Promise; /** * Remember that one occurrence has been written out. * * Keyed by occasion AND occurrence, replacing rather than appending, which is * what makes the mirror idempotent: writing the same occasion again next year * adds one record, and writing it twice this year replaces one. */ recordMirror(entry: OccasionMirrorRecord): Promise; /** * Drop every record belonging to one occasion. * * Called when the owner removes an occasion. People divorce and people die; * removing an occasion takes one sentence and one confirm, and it must not * leave last year's "no" and a gift history for a person who is no longer in * their life sitting in a file they cannot see. */ dropOccasion(occasionId: string): Promise; /** * The periodic reap. * * Four rules, each with an owner-visible consequence: * * - An answer whose occurrence has passed is dropped, so next year asks * fresh and carries no memory of the refusal. A one-off answer has no * expiry and stays: "handled" is permanent for something that happens once. * - State for an occasion no longer declared is orphaned and dropped. This is * the safety net behind the explicit removal path, for the case where the * owner deleted the line in their editor rather than through a verb. * - An open item whose occurrence has passed stops being raised. Nothing * unresolved is ever dropped WHILE IT CAN STILL MATTER; a birthday that * was three weeks ago cannot. * - Gift history ages out at the configured retention rather than never, * because a persisted store with no reaper is unbounded by design. */ sweep(input: OccasionSweepInput): Promise; /** What this store is holding, and what the last sweep removed. */ disclose(): Promise; /** Settles when every queued write has finished. Test and shutdown seam. */ drain(): Promise; } export {}; //# sourceMappingURL=state-store.d.ts.map