Reference
Functions
Section titled “Functions”createYrnkBuilder
Section titled “createYrnkBuilder”createYrnkBuilder(options?: YrnkBuilderOptions): YrnkBuilder
The store — the one stateful object of this package. One draft, one “something changed” signal: finer subscription granularity is a binding’s concern, and this minimal contract is exactly what React’s useSyncExternalStore consumes in one line.
The derived values are pure over (snapshot, resolvers), so the expensive one — the exit through core’s parse — is memoized per that pair. Swapping resolvers replaces the snapshot identity even though no field changed: the derived values change, and identity is the signal subscribers watch.
BuilderOp
Section titled “BuilderOp”type BuilderOp = // document | { readonly type: 'document/set-label'; readonly value: string } | { readonly type: 'document/set-description'; readonly value: string } | { readonly type: 'document/set-timezone'; readonly value: string } // bulk | { readonly type: 'document/replace'; readonly draft: DraftDocument } | { readonly type: 'document/load'; readonly document: YrnkDocument } // resolvers | { readonly type: 'resolvers/add' } | { readonly type: 'resolvers/set'; readonly id: DraftId; readonly name: string } | { readonly type: 'resolvers/remove'; readonly id: DraftId } // calendar — the three built-in date-list positions | { readonly type: 'calendar/set-date-set-mode'; readonly target: DateSetTarget; readonly mode: 'unset' | 'list' | 'name'; } | { readonly type: 'calendar/set-date-set-name'; readonly target: DateSetTarget; readonly name: string; } | { readonly type: 'calendar/add-date'; readonly target: DateSetTarget } | { readonly type: 'calendar/set-date'; readonly target: DateSetTarget; readonly id: DraftId; readonly date: string; } | { readonly type: 'calendar/remove-date'; readonly target: DateSetTarget; readonly id: DraftId } // calendar — workweek and business hours | { readonly type: 'calendar/set-workweek'; readonly days: readonly YrnkDayName[] } | { readonly type: 'calendar/add-business-hours-window' } | { readonly type: 'calendar/set-business-hours-window'; readonly id: DraftId; readonly start: string; readonly end: string; } | { readonly type: 'calendar/remove-business-hours-window'; readonly id: DraftId } // calendar — the date_sets namespace | { readonly type: 'calendar/add-date-set' } | { readonly type: 'calendar/rename-date-set'; readonly id: DraftId; readonly name: string } | { readonly type: 'calendar/remove-date-set'; readonly id: DraftId } | { readonly type: 'calendar/add-date-set-date'; readonly setId: DraftId } | { readonly type: 'calendar/set-date-set-date'; readonly setId: DraftId; readonly id: DraftId; readonly date: string; } | { readonly type: 'calendar/remove-date-set-date'; readonly setId: DraftId; readonly id: DraftId; } // schedules list | { readonly type: 'schedules/add' } | { readonly type: 'schedules/remove'; readonly id: DraftId } | { readonly type: 'schedules/move'; readonly id: DraftId; readonly to: number } // one schedule | { readonly type: 'schedule/set-label'; readonly scheduleId: DraftId; readonly value: string } | { readonly type: 'schedule/set-description'; readonly scheduleId: DraftId; readonly value: string; } | { readonly type: 'schedule/set-from'; readonly scheduleId: DraftId; readonly value: string } | { readonly type: 'schedule/set-until'; readonly scheduleId: DraftId; readonly value: string } | { readonly type: 'schedule/add-year'; readonly scheduleId: DraftId } | { readonly type: 'schedule/set-year'; readonly scheduleId: DraftId; readonly id: DraftId; readonly value: string; } | { readonly type: 'schedule/remove-year'; readonly scheduleId: DraftId; readonly id: DraftId } | { readonly type: 'schedule/add-month'; readonly scheduleId: DraftId } | { readonly type: 'schedule/set-month'; readonly scheduleId: DraftId; readonly id: DraftId; readonly value: string; } | { readonly type: 'schedule/remove-month'; readonly scheduleId: DraftId; readonly id: DraftId } | { readonly type: 'schedule/add-day-atom'; readonly scheduleId: DraftId } | { readonly type: 'schedule/set-day-atom'; readonly scheduleId: DraftId; readonly id: DraftId; readonly atom: DraftDayAtom; } | { readonly type: 'schedule/remove-day-atom'; readonly scheduleId: DraftId; readonly id: DraftId; } | { readonly type: 'schedule/set-shift'; readonly scheduleId: DraftId; readonly shift: DraftShift | null; } | { readonly type: 'schedule/set-if'; readonly scheduleId: DraftId; readonly if: DraftIf | null } | { readonly type: 'schedule/set-time-kind'; readonly scheduleId: DraftId; readonly kind: DraftTimeSpec['kind']; } | { readonly type: 'schedule/add-time'; readonly scheduleId: DraftId } | { readonly type: 'schedule/set-time'; readonly scheduleId: DraftId; readonly id: DraftId; readonly value: string; } | { readonly type: 'schedule/remove-time'; readonly scheduleId: DraftId; readonly id: DraftId } | { readonly type: 'schedule/set-grid-every'; readonly scheduleId: DraftId; readonly count: string; readonly unit: YrnkTimeUnit | null; } | { readonly type: 'schedule/set-grid-between'; readonly scheduleId: DraftId; readonly between: DraftBetween; } | { readonly type: 'schedule/set-sequence-every'; readonly scheduleId: DraftId; readonly count: string; readonly unit: YrnkTimeUnit | null; };The closed set of editing operations. An op writes the draft and nothing else — no validation, no refusal: the draft accepts what was typed, validity is derived afterwards (errors / optionsAt), and “don’t offer that” is the UI’s move, informed by optionsAt. Two classes of caller bug throw instead of writing: addressing an id that does not exist, and aiming a form-specific op at another form (a date-list op at a position not in list mode, a times / grid / sequence op at another time form).
Granularity: lists whose elements carry ids get element-level ops (add / set / remove); small sum-typed values (an atom, shift, if, between) are set whole — the UI builds the value, the op places it.
DateSetTarget
Section titled “DateSetTarget”type DateSetTarget = 'holidays' | 'business-holidays' | 'business-days';Which built-in date-list position of the calendar an op addresses.
DraftBetween
Section titled “DraftBetween”type DraftBetween = | { readonly kind: 'whole-day' } | { readonly kind: 'window'; readonly start: string; readonly end: string } | { readonly kind: 'business-hour' };The grid’s between position — a closed choice among the whole day, an explicit window, and the business_hour word, so no null form is needed.
DraftCalendar
Section titled “DraftCalendar”type DraftCalendar = { readonly holidays: DraftDateSetPosition; readonly businessHolidays: DraftDateSetPosition; readonly businessDays: DraftDateSetPosition; /** [] reads as "key omitted" (the default Mon–Fri workweek) */ readonly workweek: readonly YrnkDayName[]; /** [] reads as "key omitted" */ readonly businessHours: readonly DraftEntry<{ readonly start: string; readonly end: string }>[]; readonly dateSets: readonly DraftDateSetEntry[];};The calendar under edit: the three built-in date-list positions, the workweek, business hours, and the open date_sets namespace.
DraftDateSetEntry
Section titled “DraftDateSetEntry”type DraftDateSetEntry = { readonly id: DraftId; readonly name: string; readonly dates: readonly DraftEntry<string>[];};An entry of date_sets. The model holds a Record, but a draft holds a list of named entries: renaming is then an in-place text edit, and a duplicated name is representable mid-edit (parse rejects it at the exit).
DraftDateSetPosition
Section titled “DraftDateSetPosition”type DraftDateSetPosition = | { readonly mode: 'unset' } | { readonly mode: 'list'; readonly dates: readonly DraftEntry<string>[] } | { readonly mode: 'name'; readonly name: string };A built-in date-list position (holidays / business_holidays / business_days). An explicit empty list is a meaningful statement (“there are no such days”), so absence needs its own mode rather than the empty-equals-omitted reading.
DraftDayAtom
Section titled “DraftDayAtom”type DraftDayAtom = | { readonly kind: null } | { readonly kind: 'month-day'; readonly day: string } | { readonly kind: 'weekday'; readonly day: YrnkDayName | null } | { readonly kind: 'ordinal-weekday'; readonly ordinal: YrnkOrdinal | null; readonly day: YrnkDayName | null; } | { readonly kind: 'last-day-of-month' } | { readonly kind: 'calendar-word'; readonly word: YrnkCalendarWord | null } | { readonly kind: 'name'; readonly name: string } | { readonly kind: 'day-cycle'; readonly interval: string };One atom of the days enumeration: the model’s forms with their leaves loosened, plus kind: null while the form is not chosen yet.
DraftDayAtomEntry
Section titled “DraftDayAtomEntry”type DraftDayAtomEntry = { readonly id: DraftId; readonly atom: DraftDayAtom };A days-list element wrapped for identity — the day atom’s counterpart of DraftEntry.
DraftDayCondition
Section titled “DraftDayCondition”type DraftDayCondition = Exclude<DraftDayAtom, { kind: 'day-cycle' }>;A day atom legal as a shift / if condition — every form except the day cycle.
DraftDocument
Section titled “DraftDocument”type DraftDocument = { /** '' reads as "key omitted" */ readonly label: string; readonly description: string; /** * The declared spec version the draft carries: the loaded document's * own, or the latest supported version for a fresh draft. No op edits * it — an editor edits the document, not its version; moving to the * latest is the exit's decision (the builder's migrate option). */ readonly version: string; /** Always written out: a document cannot omit its timezone */ readonly timezone: string; /** [] reads as "key omitted" */ readonly resolvers: readonly DraftEntry<string>[]; readonly calendar: DraftCalendar; /** [] is spellable but invalid; parse rejects it at the exit */ readonly schedules: readonly DraftSchedule[];};The draft model’s root — the document model of @yarunoka/core mirrored with every node loosened to tolerate work in progress. Three loosenings and nothing else:
-
A freely-typed leaf (a time, a date, a boundary, an integer, a name, the timezone, an annotation) holds whatever string was typed. Validation is derived at read time; the draft never stores its own error state.
-
A closed-set leaf holds its value or null (not chosen yet), and a structural choice (the time form, the day atom form) holds kind: null the same way.
-
“Absent” splits by whether the document could spell it: where an empty spelling is invalid (annotations, boundaries, the axes, the resolvers list, workweek, business hours), the empty string / empty list reads as “key omitted”; where an explicit empty list is a meaningful statement (a date list), an explicit mode tells the forms apart.
DraftEntry
Section titled “DraftEntry”type DraftEntry<T> = { readonly id: DraftId; readonly value: T };A list element wrapped for identity: the id names the slot, not the value.
DraftEveryTuple
Section titled “DraftEveryTuple”type DraftEveryTuple = { readonly count: string; readonly unit: YrnkTimeUnit | null;};The every tuple under edit: the count as typed, and the unit or null.
DraftId
Section titled “DraftId”type DraftId = string;Opaque, draft-only; the UI’s list key and the ops’ address. toYrnk drops it.
DraftIf
Section titled “DraftIf”type DraftIf = { /** * null means "the day itself", exactly as in the model — the default * is itself a valid choice, so "not chosen yet" needs no extra state */ readonly direction: YrnkDirection | null; readonly negated: boolean; readonly condition: DraftDayCondition;};The if modifier under edit — filtering without moving, as in the model.
DraftPath
Section titled “DraftPath”type DraftPath = readonly string[];Where a problem sits in the draft: field names and draft ids, outermost first (e.g. [‘schedules’, ‘s3’, ‘from’]). Ids rather than indexes, so the path survives list edits around it.
DraftProblem
Section titled “DraftProblem”type DraftProblem = { readonly path: DraftPath; /** Wording comes from @yarunoka/core wherever core has the rule */ readonly message: string; readonly origin: 'field' | 'document';};One validation finding. A field problem is a core validation helper’s answer placed at its draft path; a document problem is core’s parse() rejecting the whole (it carries no path — it appears only once every field is individually clean).
DraftSchedule
Section titled “DraftSchedule”type DraftSchedule = { readonly id: DraftId; readonly label: string; readonly description: string; /** '' reads as "key omitted" */ readonly from: string; readonly until: string; /** [] reads as "axis absent" (no restriction) */ readonly years: readonly DraftEntry<string>[]; readonly months: readonly DraftEntry<string>[]; readonly days: readonly DraftDayAtomEntry[]; readonly shift: DraftShift | null; readonly if: DraftIf | null; readonly time: DraftTimeSpec;};One schedule under edit. The shape mirrors the model’s schedule — the axes, the modifiers, the time part — with the id as the schedule’s address for ops, options, and problem paths.
DraftShift
Section titled “DraftShift”type DraftShift = { readonly direction: YrnkDirection | null; readonly orSame: boolean; readonly condition: DraftDayCondition;};The shift modifier under edit. Unlike the model’s shift, direction may be null — a shift can exist before its direction is chosen.
DraftTimeSpec
Section titled “DraftTimeSpec”type DraftTimeSpec = | { readonly kind: null } | { readonly kind: 'times'; readonly times: readonly DraftEntry<string>[] } | { readonly kind: 'grid'; readonly every: DraftEveryTuple; readonly between: DraftBetween } | { readonly kind: 'allday' } | { readonly kind: 'sequence'; readonly every: DraftEveryTuple };The time part under edit: one of the model’s four forms, or kind: null while the form is not chosen yet.
OptionContext
Section titled “OptionContext”type OptionContext = | { readonly at: 'day-atom-kind'; readonly scheduleId: DraftId } | { readonly at: 'condition-kind'; readonly scheduleId: DraftId; readonly of: 'shift' | 'if' } | { readonly at: 'time-kind'; readonly scheduleId: DraftId } | { readonly at: 'between-kind'; readonly scheduleId: DraftId } | { readonly at: 'calendar-word'; readonly scheduleId: DraftId } | { readonly at: 'name'; readonly scheduleId: DraftId };A decision point the UI can ask about. Every context is addressed through a schedule: the questions are asked while editing one, and the answers that depend on the document (the calendar, the names) read it through the draft as a whole.
Options
Section titled “Options”type Options = readonly { readonly value: string; readonly available: boolean; /** Why not, when available is false */ readonly reason?: string;}[];What a decision point offers: every value of its closed set, each marked available or not. Unavailable options are answered rather than dropped — whether to hide or to disable is the UI’s decision.
PreviewOccurrence
Section titled “PreviewOccurrence”type PreviewOccurrence = { readonly occurrence: YrnkOccurrence; /** The draft schedules this occurrence came from */ readonly scheduleIds: readonly DraftId[];};One line of a preview: the occurrence as core answers it, and which draft schedules produced it.
PreviewRequest
Section titled “PreviewRequest”type PreviewRequest = | { /** How many upcoming occurrences to answer */ readonly next: number; /** * The instant the answered occurrences lie strictly after; the * current instant when omitted */ readonly after?: YrnkInstant; /** How far the search may reach; ten years when omitted */ readonly horizon?: Temporal.Duration; } | { readonly range: { readonly from: YrnkInstant; readonly through: YrnkInstant } };What to preview: the next N occurrences after an instant, or every occurrence in an explicit range.
PreviewResult
Section titled “PreviewResult”type PreviewResult = | { readonly ok: true; readonly occurrences: readonly PreviewOccurrence[]; /** True when the horizon ended a "next N" search short of N */ readonly exhausted: boolean; } | { readonly ok: false; readonly problems: readonly DraftProblem[] };What preview() answers: the occurrences and whether the horizon cut a “next N” search short, or — when the draft cannot export — the same problems errors() reports.
ToYrnkResult
Section titled “ToYrnkResult”type ToYrnkResult = | { readonly ok: true; readonly document: YrnkDocument; readonly raw: Record<string, unknown> } | { readonly ok: false; readonly problems: readonly DraftProblem[] };The exit’s answer: the parsed document with its raw spelling when the draft exports cleanly, or the problems that keep it from the wire.
YrnkBuilder
Section titled “YrnkBuilder”type YrnkBuilder = { /** The current draft — an immutable snapshot */ getState(): DraftDocument; /** Change notification; the returned function unsubscribes */ subscribe(listener: () => void): () => void; dispatch(op: BuilderOp): void; setResolvers(resolvers: Readonly<Record<string, YrnkResolver>>): void;
errors(): readonly DraftProblem[]; optionsAt(context: OptionContext): Options; toYrnk(): ToYrnkResult; preview(request: PreviewRequest): PreviewResult;};The store’s handle: the external-store trio (getState / subscribe / dispatch) plus the derived values an editor reads — validation, options, the strict document, and upcoming occurrences.
YrnkBuilderOptions
Section titled “YrnkBuilderOptions”type YrnkBuilderOptions = { /** The document to start editing; an empty new draft when omitted */ readonly initial?: YrnkDocument; /** What the host binds the draft's declared resolver names to */ readonly resolvers?: Readonly<Record<string, YrnkResolver>>; /** * Export on the latest supported spec version instead of the version * the loaded document declares (a fresh draft starts on the latest * either way). Off by default: opening and saving a document does not * change what version it declares unless the host asks for that. */ readonly migrate?: boolean;};What createYrnkBuilder accepts: the starting document, the host’s resolver bindings, and whether exports migrate to the latest spec version.