@mulmoclaude/core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/assets/helps/google-calendar-collection.md +57 -3
  2. package/assets/helps/google.md +3 -2
  3. package/dist/calendarGrid-CaS9er8i.cjs +576 -0
  4. package/dist/calendarGrid-CaS9er8i.cjs.map +1 -0
  5. package/dist/calendarGrid-CrGxeCOG.js +397 -0
  6. package/dist/calendarGrid-CrGxeCOG.js.map +1 -0
  7. package/dist/collection/index.cjs +44 -44
  8. package/dist/collection/index.cjs.map +1 -1
  9. package/dist/collection/index.js +2 -2
  10. package/dist/collection/registry/server/index.cjs +2 -2
  11. package/dist/collection/registry/server/index.js +2 -2
  12. package/dist/collection/server/index.cjs +4 -4
  13. package/dist/collection/server/index.js +3 -3
  14. package/dist/collection-watchers/index.cjs +5 -5
  15. package/dist/collection-watchers/index.cjs.map +1 -1
  16. package/dist/collection-watchers/index.js +4 -4
  17. package/dist/{discovery--kRUWed0.cjs → discovery-DYcR83qx.cjs} +64 -53
  18. package/dist/discovery-DYcR83qx.cjs.map +1 -0
  19. package/dist/{discovery-CKR4TOX3.js → discovery-_fjsluLx.js} +43 -32
  20. package/dist/discovery-_fjsluLx.js.map +1 -0
  21. package/dist/feeds/index.cjs +4 -4
  22. package/dist/feeds/index.js +2 -2
  23. package/dist/feeds/server/index.cjs +6 -6
  24. package/dist/feeds/server/index.js +4 -4
  25. package/dist/google/apiClient.d.ts +6 -0
  26. package/dist/google/calendar.d.ts +79 -8
  27. package/dist/google/calendarPushState.d.ts +17 -0
  28. package/dist/google/collectionPush.d.ts +64 -0
  29. package/dist/google/collectionSync.d.ts +10 -0
  30. package/dist/google/index.cjs +712 -36
  31. package/dist/google/index.cjs.map +1 -1
  32. package/dist/google/index.d.ts +7 -3
  33. package/dist/google/index.js +683 -37
  34. package/dist/google/index.js.map +1 -1
  35. package/dist/google/pushDateTime.d.ts +10 -0
  36. package/dist/google/pushPlan.d.ts +73 -0
  37. package/dist/{ingestTypes-xX8VpQqh.cjs → ingestTypes-D1GQdG8e.cjs} +3 -3
  38. package/dist/{ingestTypes-xX8VpQqh.cjs.map → ingestTypes-D1GQdG8e.cjs.map} +1 -1
  39. package/dist/{ingestTypes-L59cIGgX.js → ingestTypes-XReG-li7.js} +2 -2
  40. package/dist/{ingestTypes-L59cIGgX.js.map → ingestTypes-XReG-li7.js.map} +1 -1
  41. package/dist/{promptSafety-CQ5Un4wi.js → promptSafety-C--op_6a.js} +3 -269
  42. package/dist/promptSafety-C--op_6a.js.map +1 -0
  43. package/dist/{promptSafety-DfZYNtjK.cjs → promptSafety-FD8cQn3e.cjs} +8 -358
  44. package/dist/promptSafety-FD8cQn3e.cjs.map +1 -0
  45. package/dist/remote-host/index.cjs +8 -0
  46. package/dist/remote-host/index.cjs.map +1 -1
  47. package/dist/remote-host/index.d.ts +21 -0
  48. package/dist/remote-host/index.js +8 -1
  49. package/dist/remote-host/index.js.map +1 -1
  50. package/dist/{server-D_6FLygK.js → server-BlIrtGKl.js} +4 -4
  51. package/dist/{server-D_6FLygK.js.map → server-BlIrtGKl.js.map} +1 -1
  52. package/dist/{server-gEOeV_Nv.cjs → server-l4TZ5Lbf.cjs} +21 -21
  53. package/dist/{server-gEOeV_Nv.cjs.map → server-l4TZ5Lbf.cjs.map} +1 -1
  54. package/package.json +1 -1
  55. package/dist/discovery--kRUWed0.cjs.map +0 -1
  56. package/dist/discovery-CKR4TOX3.js.map +0 -1
  57. package/dist/ids-BnTh3Hl0.cjs +0 -226
  58. package/dist/ids-BnTh3Hl0.cjs.map +0 -1
  59. package/dist/ids-s4GfmE93.js +0 -131
  60. package/dist/ids-s4GfmE93.js.map +0 -1
  61. package/dist/promptSafety-CQ5Un4wi.js.map +0 -1
  62. package/dist/promptSafety-DfZYNtjK.cjs.map +0 -1
@@ -6,10 +6,22 @@ events on a schedule and writes them as records — **without calling you**. No
6
6
  tool call, no tokens spent per sync, so hourly syncing is free.
7
7
 
8
8
  This is the mechanism for *keeping a collection fresh*. For one-off reads and
9
- for creating / editing / deleting events, use the `google` tool — see
9
+ for deleting events, use the `google` tool — see
10
10
  [The `google` tool](google.md). There is no bundled calendar collection: you
11
11
  author the schema when the user asks for one.
12
12
 
13
+ ## Both directions, but only one of them automatic
14
+
15
+ | Direction | How | When it runs |
16
+ | --- | --- | --- |
17
+ | Google → collection | the `googleCalendar` block | hourly, on creation, and on the **Sync** button |
18
+ | collection → Google | the **Push to Google** button in the collection view | only when the user clicks it |
19
+
20
+ There is no automatic write-back and no setting that enables one. If a user
21
+ asks for "two-way sync", tell them plainly: the pull is automatic, the push is
22
+ a button they press. Do not go looking for a config key for it — there isn't
23
+ one, and hunting for it is what makes this conversation go in circles.
24
+
13
25
  ## Requirements
14
26
 
15
27
  The user's Google account must be linked (`google` tool, `kind: "status"`). If
@@ -81,8 +93,50 @@ the calendar day view then draws each record as a proportional time block.
81
93
  records on that first pass.
82
94
 
83
95
  Records are ordinary collection records: the user can open, filter, and view
84
- them like any other. Edits they make locally are overwritten the next time
85
- Google reports a change to that event.
96
+ them like any other.
97
+
98
+ ## Pushing local work back — the Push to Google button
99
+
100
+ The collection view has a **Push to Google** button next to Sync. It creates
101
+ events for records added locally and updates events for records edited locally.
102
+
103
+ **Order matters, and this is the one thing to warn users about.** A pull
104
+ overwrites a locally edited record as soon as Google reports any change to that
105
+ event. So: push first, then sync. Syncing first can discard the edit that was
106
+ waiting to be pushed.
107
+
108
+ What the button does and deliberately does not do:
109
+
110
+ - **Creates** an event for a record that never came from a sync.
111
+ - **Updates** only the fields the user actually changed, so attendees,
112
+ reminders and recurrence rules stay untouched.
113
+ - **Never deletes.** A record deleted locally leaves its Google event alone —
114
+ a Google delete removes the event for every attendee and cannot be undone.
115
+ The count is reported so the user knows it was skipped; deleting for real is
116
+ the `google` tool's `calendarDeleteEvent`, after confirming with them.
117
+ - **Skips a record edited on both sides** and reports it, rather than picking a
118
+ winner. The user resolves it by editing one side to match.
119
+ - Pushes only `summary`, `start`, `end` and `colorId` — `htmlLink` and `status`
120
+ are read-only in Google, so a column mapped to either is ignored.
121
+
122
+ Reasons a record can be reported as skipped:
123
+
124
+ - **Its record id cannot be a Google event id.** Google requires 5-1024
125
+ characters from `0-9a-v`. Records created through the UI get a valid
126
+ generated id; a semantic id you authored (`team-standup`) cannot be used. Fix
127
+ by recreating the record without setting the primary field.
128
+ - **No `start` / `end` is mapped, on a record being CREATED.** An event cannot
129
+ be created without a span. Editing an existing event is unaffected — a changed
130
+ title or colour is patched on its own.
131
+ - **The calendar reports no timezone**, and the stored clock carries no offset
132
+ to fall back on.
133
+ - **Clearing an event colour** — Google has no way to unset one.
134
+
135
+ If the user only has `reader` access to the calendar, the whole push is refused
136
+ with that reason rather than failing event by event. A calendar the user can
137
+ reach by id but has not added to their calendar list has no role to check, so
138
+ the push goes ahead and reports Google's own refusal if the write turns out not
139
+ to be allowed — being unlisted is not treated as being read-only.
86
140
 
87
141
  ## Not for this
88
142
 
@@ -15,10 +15,11 @@ the original call — do not fall back to guessing.
15
15
  | --- | --- |
16
16
  | Read or change specific events / tasks / files, right now | this tool |
17
17
  | A collection that mirrors a calendar and keeps itself fresh | a `googleCalendar` block — see [Google Calendar sync](google-calendar-collection.md) |
18
+ | Records the user added/edited in a calendar collection to reach Google | the collection view's **Push to Google** button, not this tool |
18
19
 
19
20
  The collection route costs no tokens per refresh, so prefer it for anything
20
- recurring. Use this tool for one-off work and for everything the collection
21
- route cannot do (creating, editing and deleting).
21
+ recurring. Use this tool for one-off work, and for deleting — the push button
22
+ never deletes.
22
23
 
23
24
  ## Calendar
24
25
 
@@ -0,0 +1,576 @@
1
+ //#region src/collection/core/fieldText.ts
2
+ /** A field value that has a meaningful text form. Dates are included because
3
+ * a JSON record can carry one once it has been revived. */
4
+ var isTextable = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
5
+ /** The field's text, or `null` when it has no meaningful one (absent, or an
6
+ * array/object that would stringify to `"[object Object]"`).
7
+ *
8
+ * Returning `null` rather than `""` keeps "the field is empty" distinct from
9
+ * "the field can't be text" — a matcher must not treat an object-valued field
10
+ * as an empty string and match `""`. */
11
+ function fieldTextOrNull(value) {
12
+ if (value === void 0 || value === null) return null;
13
+ if (!isTextable(value)) return null;
14
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
15
+ return String(value);
16
+ }
17
+ /** The field's text, or `fallback` (default `""`) when it has none. Use where
18
+ * a string is required and an empty one is a safe stand-in — display, sort
19
+ * keys, CSV cells. Where the distinction matters, use {@link fieldTextOrNull}. */
20
+ function fieldText(value, fallback = "") {
21
+ return fieldTextOrNull(value) ?? fallback;
22
+ }
23
+ //#endregion
24
+ //#region src/collection/core/schema.ts
25
+ /** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's
26
+ * feeds engine dispatches on these; they live here (with the schema contract)
27
+ * so the schema validator can enforce them. The host re-exports these from
28
+ * `server/workspace/feeds/ingestTypes.ts`. */
29
+ var INGEST_KINDS = [
30
+ "rss",
31
+ "atom",
32
+ "http-json"
33
+ ];
34
+ /** The agent-performed ingest kind. Instead of a declarative fetch, the host
35
+ * dispatches a hidden background chat (origin `system`) in `ingest.role`,
36
+ * seeded with `ingest.template` + a summary of every record, on the
37
+ * `ingest.schedule` cadence; the worker edits records via the collections io
38
+ * layer. Kept separate from {@link INGEST_KINDS} (which the declarative
39
+ * retriever registry keys on) so the schema validator can model `ingest` as a
40
+ * discriminated union without the feeds engine gaining an "agent" retriever. */
41
+ var AGENT_INGEST_KIND = "agent";
42
+ /** Refresh cadences a Feed's `ingest.schedule` may declare. */
43
+ var FEED_SCHEDULES = [
44
+ "hourly",
45
+ "daily",
46
+ "weekly",
47
+ "on-demand"
48
+ ];
49
+ /** derived/embed/backlinks/rollup/toggle/flag are host-computed or
50
+ * projected — never written to the record JSON, so required / value
51
+ * checks and edit-draft slots must not apply to them. THE single source
52
+ * for "computed" — lives here (zod-free at runtime) so browser code
53
+ * (`./draft`) and the zod record compiler (`./recordZ`, which re-exports
54
+ * it) share one set instead of drifting copies. */
55
+ var COMPUTED_TYPES = /* @__PURE__ */ new Set([
56
+ "derived",
57
+ "embed",
58
+ "backlinks",
59
+ "rollup",
60
+ "toggle",
61
+ "flag"
62
+ ]);
63
+ /** Narrowing guard: true when `every` is the field-driven arm. */
64
+ function isFieldDrivenEvery(every) {
65
+ return "fromField" in every;
66
+ }
67
+ /** Which storage backend serves this schema's records. Derived, not stored:
68
+ * existing schemas carry no `storage` key and must keep resolving exactly
69
+ * as before (`dataSource` ⇒ csv, else file). */
70
+ function storageKindFor(schema) {
71
+ if (schema.dataSource !== void 0) return "csv";
72
+ return schema.storage?.type ?? "file";
73
+ }
74
+ /** True when `schema` declares an external `dataSource` — i.e. the
75
+ * collection is READ-ONLY through every UI/tool write path (updates
76
+ * happen by editing/replacing the data file itself). Isomorphic: both
77
+ * the server write guards and the client's control hiding key off this
78
+ * one predicate. */
79
+ function isReadOnlySchema(schema) {
80
+ return schema.dataSource !== void 0;
81
+ }
82
+ /** Resolve an `embed` field's target record id: the fixed `id`, or the value
83
+ * of the sibling `idField` on this record (empty string when neither applies
84
+ * — the caller renders that as "no record"). Pure + isomorphic so the server
85
+ * projection (`derive.ts`) and the client preview (`useCollectionRendering`)
86
+ * resolve embeds identically. Non-`embed` fields resolve to "no record". */
87
+ function embedTargetId(field, record) {
88
+ if (field.type !== "embed") return "";
89
+ if (field.id) return field.id;
90
+ if (field.idField && record) return fieldText(record[field.idField]);
91
+ return "";
92
+ }
93
+ //#endregion
94
+ //#region src/collection/core/ids.ts
95
+ var SAFE_SLUG_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
96
+ var SAFE_RECORD_ID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
97
+ /** True when `value` is a well-formed collection slug (alphanumeric /
98
+ * hyphen / underscore, no path separators). The pattern admits no `/`,
99
+ * `\`, or `.`, so a passing value is trivially also a safe basename —
100
+ * validation callers need no `path.basename` round-trip (path-building
101
+ * callers use `../server/paths#safeSlugName`, which adds it). */
102
+ function isSafeSlug(value) {
103
+ return typeof value === "string" && SAFE_SLUG_PATTERN.test(value);
104
+ }
105
+ /** True when `value` is a well-formed record id (slug charset plus interior
106
+ * dots), with any `..` substring rejected explicitly. Validation-only
107
+ * counterpart of `../server/paths#safeRecordId`. */
108
+ function isSafeRecordId(value) {
109
+ if (typeof value !== "string" || !SAFE_RECORD_ID_PATTERN.test(value)) return false;
110
+ return !value.includes("..");
111
+ }
112
+ var DEFAULT_UNIQUE_ID_ATTEMPTS = 8;
113
+ /** Pick an id not already in `existing`, re-rolling `generate()` up to
114
+ * `maxAttempts` times before giving up and returning the last candidate.
115
+ * Collisions on a wide id space are astronomically unlikely, so a caller's
116
+ * own overwrite guard is the final backstop rather than an unbounded loop. */
117
+ function generateUniqueId(existing, generate, maxAttempts = DEFAULT_UNIQUE_ID_ATTEMPTS) {
118
+ let candidate = generate();
119
+ for (let attempt = 0; attempt < maxAttempts && existing.has(candidate); attempt++) candidate = generate();
120
+ return candidate;
121
+ }
122
+ /** An id not already used by any loaded record: collects the in-memory primary
123
+ * keys and re-rolls `generate()` against them (see {@link generateUniqueId}).
124
+ * The pure core of the view's create-form id prefill. */
125
+ function nextUniqueItemId(items, primaryKey, generate, maxAttempts = DEFAULT_UNIQUE_ID_ATTEMPTS) {
126
+ return generateUniqueId(new Set(items.map((item) => fieldText(item[primaryKey]))), generate, maxAttempts);
127
+ }
128
+ //#endregion
129
+ //#region src/collection/core/calendarGrid.ts
130
+ var MS_PER_DAY = 864e5;
131
+ var ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
132
+ var TWO_DIGIT_RE = /^\d{2}$/;
133
+ var CLOCK_RE = /(\d{1,2}):(\d{2})/g;
134
+ var RANGE_SEP_RE = /[-–—~〜~]/;
135
+ /** Minutes in a full day — the timeline's vertical extent. */
136
+ var MINUTES_PER_DAY = 1440;
137
+ function pad2(value) {
138
+ return String(value).padStart(2, "0");
139
+ }
140
+ /** Canonical `YYYY-MM-DD` string for a civil date. */
141
+ function ymdKey(ymd) {
142
+ return `${String(ymd.year).padStart(4, "0")}-${pad2(ymd.month)}-${pad2(ymd.day)}`;
143
+ }
144
+ /** Strictly parse a `YYYY-MM-DD` string into a civil date, rejecting
145
+ * anything that isn't a real calendar day (e.g. `2026-02-30`, `2026-13-01`).
146
+ * Returns null for non-strings and malformed values so callers can route
147
+ * records with no usable date into the "no date" tray rather than crash. */
148
+ function parseIsoDate(value) {
149
+ if (typeof value !== "string") return null;
150
+ const match = ISO_DATE_RE.exec(value.trim());
151
+ if (!match) return null;
152
+ const year = Number(match[1]);
153
+ const month = Number(match[2]);
154
+ const day = Number(match[3]);
155
+ const probe = new Date(Date.UTC(year, month - 1, day));
156
+ if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return null;
157
+ return {
158
+ year,
159
+ month,
160
+ day
161
+ };
162
+ }
163
+ /** Minutes-of-day for an `HH:MM` pair, or null when out of range. */
164
+ function clockToMinutes(hours, minutes) {
165
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
166
+ return hours * 60 + minutes;
167
+ }
168
+ /** Strictly parse a `YYYY-MM-DDTHH:MM` (optional `:SS`) datetime into its
169
+ * civil date and minutes-of-day. Returns null for anything that isn't a real
170
+ * calendar day or a valid 24h clock. */
171
+ function parseIsoDateTime(value) {
172
+ if (typeof value !== "string") return null;
173
+ const trimmed = value.trim();
174
+ const tIndex = trimmed.indexOf("T");
175
+ if (tIndex === -1) return null;
176
+ const ymd = parseIsoDate(trimmed.slice(0, tIndex));
177
+ if (!ymd) return null;
178
+ const parts = trimmed.slice(tIndex + 1).split(":");
179
+ if (parts.length < 2 || parts.length > 3 || !parts.every((part) => TWO_DIGIT_RE.test(part))) return null;
180
+ const minutes = clockToMinutes(Number(parts[0]), Number(parts[1]));
181
+ if (minutes === null) return null;
182
+ return {
183
+ ymd,
184
+ minutes
185
+ };
186
+ }
187
+ /** Civil date from either a `YYYY-MM-DD` or a `YYYY-MM-DDTHH:MM` value, so the
188
+ * month grid buckets date-only and datetime anchors alike. */
189
+ function dateOf(value) {
190
+ return parseIsoDate(value) ?? parseIsoDateTime(value)?.ymd ?? null;
191
+ }
192
+ /** Minutes-of-day from a datetime value, or null for date-only / invalid. */
193
+ function timeOf(value) {
194
+ return parseIsoDateTime(value)?.minutes ?? null;
195
+ }
196
+ /** Parse a free-form time-string field into start/end minutes-of-day.
197
+ * Handles the common shapes in user data:
198
+ * "14:00-17:00" → { start: 840, end: 1020 } (range → block)
199
+ * "17:00-" → { start: 1020, end: null } (open end → single line)
200
+ * "16:30" → { start: 990, end: null } (point in time → single line)
201
+ * "終日" / "" → null (no clock → all-day)
202
+ * Returns null when no clock token is parseable. */
203
+ function parseTimeRange(value) {
204
+ if (typeof value !== "string") return null;
205
+ const text = value.trim();
206
+ if (!text) return null;
207
+ const tokens = [...text.matchAll(CLOCK_RE)];
208
+ if (tokens.length === 0) return null;
209
+ const minutesOf = (match) => clockToMinutes(Number(match[1]), Number(match[2]));
210
+ if (!RANGE_SEP_RE.test(text)) {
211
+ const startMin = minutesOf(tokens[0]);
212
+ return startMin === null ? null : {
213
+ startMin,
214
+ endMin: null
215
+ };
216
+ }
217
+ const sepIndex = text.search(RANGE_SEP_RE);
218
+ let startMin = null;
219
+ let endMin = null;
220
+ for (const token of tokens) if ((token.index ?? 0) < sepIndex) startMin = minutesOf(token);
221
+ else endMin = minutesOf(token);
222
+ if (startMin === null) return null;
223
+ return {
224
+ startMin,
225
+ endMin
226
+ };
227
+ }
228
+ function ymdToUtcMs(ymd) {
229
+ return Date.UTC(ymd.year, ymd.month - 1, ymd.day);
230
+ }
231
+ function utcMsToYmd(epochMs) {
232
+ const date = new Date(epochMs);
233
+ return {
234
+ year: date.getUTCFullYear(),
235
+ month: date.getUTCMonth() + 1,
236
+ day: date.getUTCDate()
237
+ };
238
+ }
239
+ /** Chronological comparison: negative if `left` precedes `right`, 0 if the
240
+ * same day, positive if after. */
241
+ function compareYmd(left, right) {
242
+ return ymdToUtcMs(left) - ymdToUtcMs(right);
243
+ }
244
+ /** True iff `day` falls within the inclusive span `[span.start, span.end]`. */
245
+ function spanCoversDay(span, day) {
246
+ return compareYmd(span.start, day) <= 0 && compareYmd(day, span.end) <= 0;
247
+ }
248
+ /** Build the 6×7 (42-cell) grid for the given month, including the
249
+ * leading/trailing days of the adjacent months so every week is full.
250
+ * `month` is 1-12. `weekStartsOn` is 0 (Sunday) … 6 (Saturday). */
251
+ function buildMonthGrid(year, month, weekStartsOn = 0) {
252
+ const lead = (new Date(Date.UTC(year, month - 1, 1)).getUTCDay() - weekStartsOn + 7) % 7;
253
+ const startMs = Date.UTC(year, month - 1, 1) - lead * MS_PER_DAY;
254
+ const cells = [];
255
+ for (let i = 0; i < 42; i++) {
256
+ const ymd = utcMsToYmd(startMs + i * MS_PER_DAY);
257
+ cells.push({
258
+ ymd,
259
+ inMonth: ymd.year === year && ymd.month === month,
260
+ key: ymdKey(ymd)
261
+ });
262
+ }
263
+ return cells;
264
+ }
265
+ /** Resolve a record's calendar span from its date/datetime fields. Returns
266
+ * null when the anchor date is missing/invalid (→ the caller's "no date"
267
+ * tray). An end date that is missing, invalid, or earlier than the start
268
+ * collapses to a single-day span — never an inverted range.
269
+ *
270
+ * Times for the day (time-allocation) view come from, in priority order:
271
+ * 1. the clock on a `datetime` anchor/end value, else
272
+ * 2. `timeField` — a separate free-form time-string column (e.g. "14:00-17:00").
273
+ * A record with no resolvable clock has `startMin === endMin === null`. */
274
+ function recordSpan(item, anchorField, endField, timeField) {
275
+ const startRaw = item[anchorField];
276
+ const start = dateOf(startRaw);
277
+ if (!start) return null;
278
+ let end = start;
279
+ let startMin = timeOf(startRaw);
280
+ let endMin = null;
281
+ if (endField) {
282
+ const endRaw = item[endField];
283
+ const parsedEnd = dateOf(endRaw);
284
+ if (parsedEnd && compareYmd(parsedEnd, start) >= 0) {
285
+ end = parsedEnd;
286
+ endMin = timeOf(endRaw);
287
+ }
288
+ }
289
+ if (timeField && startMin === null && endMin === null) {
290
+ const range = parseTimeRange(item[timeField]);
291
+ if (range) ({startMin, endMin} = range);
292
+ }
293
+ return {
294
+ item,
295
+ start,
296
+ end,
297
+ startMin,
298
+ endMin
299
+ };
300
+ }
301
+ /** Split records into those that land on the calendar (with their spans)
302
+ * and those with no usable anchor date (the "no date" tray). Spans are
303
+ * sorted by start day so same-day stacking is stable across renders. */
304
+ function bucketRecords(items, anchorField, endField, timeField) {
305
+ const spans = [];
306
+ const noDate = [];
307
+ for (const item of items) {
308
+ const span = recordSpan(item, anchorField, endField, timeField);
309
+ if (span) spans.push(span);
310
+ else noDate.push(item);
311
+ }
312
+ spans.sort((left, right) => compareYmd(left.start, right.start));
313
+ return {
314
+ spans,
315
+ noDate
316
+ };
317
+ }
318
+ /** Project a record's span onto a single day for the time-allocation view, or
319
+ * null when the span doesn't cover that day. */
320
+ function daySlice(span, day) {
321
+ if (!spanCoversDay(span, day)) return null;
322
+ const hasStart = span.startMin !== null;
323
+ const hasEnd = span.endMin !== null;
324
+ if (!hasStart && !hasEnd) return {
325
+ kind: "allDay",
326
+ startMin: 0,
327
+ endMin: MINUTES_PER_DAY,
328
+ bleedsBefore: false,
329
+ bleedsAfter: false
330
+ };
331
+ const singleDay = compareYmd(span.start, span.end) === 0;
332
+ const isStartDay = compareYmd(day, span.start) === 0;
333
+ const isEndDay = compareYmd(day, span.end) === 0;
334
+ if (singleDay && hasStart && !hasEnd) return {
335
+ kind: "line",
336
+ startMin: span.startMin,
337
+ endMin: span.startMin,
338
+ bleedsBefore: false,
339
+ bleedsAfter: false
340
+ };
341
+ const startMin = isStartDay && hasStart ? span.startMin : 0;
342
+ const endMin = isEndDay && hasEnd ? span.endMin : MINUTES_PER_DAY;
343
+ if (singleDay && endMin <= startMin) return {
344
+ kind: "line",
345
+ startMin,
346
+ endMin: startMin,
347
+ bleedsBefore: false,
348
+ bleedsAfter: false
349
+ };
350
+ return {
351
+ kind: "block",
352
+ startMin,
353
+ endMin,
354
+ bleedsBefore: !isStartDay,
355
+ bleedsAfter: !isEndDay
356
+ };
357
+ }
358
+ function assignLanes(blocks) {
359
+ const order = [...blocks.keys()].sort((left, right) => blocks[left].startMin - blocks[right].startMin || blocks[left].endMin - blocks[right].endMin);
360
+ const result = blocks.map(() => ({
361
+ lane: 0,
362
+ lanes: 1
363
+ }));
364
+ let cluster = [];
365
+ let clusterEnd = Number.NEGATIVE_INFINITY;
366
+ const laneEnds = [];
367
+ const flush = () => {
368
+ for (const index of cluster) result[index].lanes = laneEnds.length;
369
+ cluster = [];
370
+ laneEnds.length = 0;
371
+ clusterEnd = Number.NEGATIVE_INFINITY;
372
+ };
373
+ for (const index of order) {
374
+ const block = blocks[index];
375
+ if (cluster.length > 0 && block.startMin >= clusterEnd) flush();
376
+ let lane = laneEnds.findIndex((end) => end <= block.startMin);
377
+ if (lane === -1) {
378
+ lane = laneEnds.length;
379
+ laneEnds.push(block.endMin);
380
+ } else laneEnds[lane] = block.endMin;
381
+ result[index].lane = lane;
382
+ cluster.push(index);
383
+ clusterEnd = Math.max(clusterEnd, block.endMin);
384
+ }
385
+ flush();
386
+ return result;
387
+ }
388
+ /** Month label key inputs — returns the 1st of the month as a `Date` so the
389
+ * component can feed it to `Intl.DateTimeFormat(locale, …)` for a localized
390
+ * "April 2026" header without this module taking a locale dependency. */
391
+ function monthAnchorDate(year, month) {
392
+ return new Date(Date.UTC(year, month - 1, 1));
393
+ }
394
+ //#endregion
395
+ Object.defineProperty(exports, "AGENT_INGEST_KIND", {
396
+ enumerable: true,
397
+ get: function() {
398
+ return AGENT_INGEST_KIND;
399
+ }
400
+ });
401
+ Object.defineProperty(exports, "COMPUTED_TYPES", {
402
+ enumerable: true,
403
+ get: function() {
404
+ return COMPUTED_TYPES;
405
+ }
406
+ });
407
+ Object.defineProperty(exports, "FEED_SCHEDULES", {
408
+ enumerable: true,
409
+ get: function() {
410
+ return FEED_SCHEDULES;
411
+ }
412
+ });
413
+ Object.defineProperty(exports, "INGEST_KINDS", {
414
+ enumerable: true,
415
+ get: function() {
416
+ return INGEST_KINDS;
417
+ }
418
+ });
419
+ Object.defineProperty(exports, "MINUTES_PER_DAY", {
420
+ enumerable: true,
421
+ get: function() {
422
+ return MINUTES_PER_DAY;
423
+ }
424
+ });
425
+ Object.defineProperty(exports, "SAFE_RECORD_ID_PATTERN", {
426
+ enumerable: true,
427
+ get: function() {
428
+ return SAFE_RECORD_ID_PATTERN;
429
+ }
430
+ });
431
+ Object.defineProperty(exports, "SAFE_SLUG_PATTERN", {
432
+ enumerable: true,
433
+ get: function() {
434
+ return SAFE_SLUG_PATTERN;
435
+ }
436
+ });
437
+ Object.defineProperty(exports, "assignLanes", {
438
+ enumerable: true,
439
+ get: function() {
440
+ return assignLanes;
441
+ }
442
+ });
443
+ Object.defineProperty(exports, "bucketRecords", {
444
+ enumerable: true,
445
+ get: function() {
446
+ return bucketRecords;
447
+ }
448
+ });
449
+ Object.defineProperty(exports, "buildMonthGrid", {
450
+ enumerable: true,
451
+ get: function() {
452
+ return buildMonthGrid;
453
+ }
454
+ });
455
+ Object.defineProperty(exports, "compareYmd", {
456
+ enumerable: true,
457
+ get: function() {
458
+ return compareYmd;
459
+ }
460
+ });
461
+ Object.defineProperty(exports, "dateOf", {
462
+ enumerable: true,
463
+ get: function() {
464
+ return dateOf;
465
+ }
466
+ });
467
+ Object.defineProperty(exports, "daySlice", {
468
+ enumerable: true,
469
+ get: function() {
470
+ return daySlice;
471
+ }
472
+ });
473
+ Object.defineProperty(exports, "embedTargetId", {
474
+ enumerable: true,
475
+ get: function() {
476
+ return embedTargetId;
477
+ }
478
+ });
479
+ Object.defineProperty(exports, "fieldText", {
480
+ enumerable: true,
481
+ get: function() {
482
+ return fieldText;
483
+ }
484
+ });
485
+ Object.defineProperty(exports, "fieldTextOrNull", {
486
+ enumerable: true,
487
+ get: function() {
488
+ return fieldTextOrNull;
489
+ }
490
+ });
491
+ Object.defineProperty(exports, "generateUniqueId", {
492
+ enumerable: true,
493
+ get: function() {
494
+ return generateUniqueId;
495
+ }
496
+ });
497
+ Object.defineProperty(exports, "isFieldDrivenEvery", {
498
+ enumerable: true,
499
+ get: function() {
500
+ return isFieldDrivenEvery;
501
+ }
502
+ });
503
+ Object.defineProperty(exports, "isReadOnlySchema", {
504
+ enumerable: true,
505
+ get: function() {
506
+ return isReadOnlySchema;
507
+ }
508
+ });
509
+ Object.defineProperty(exports, "isSafeRecordId", {
510
+ enumerable: true,
511
+ get: function() {
512
+ return isSafeRecordId;
513
+ }
514
+ });
515
+ Object.defineProperty(exports, "isSafeSlug", {
516
+ enumerable: true,
517
+ get: function() {
518
+ return isSafeSlug;
519
+ }
520
+ });
521
+ Object.defineProperty(exports, "monthAnchorDate", {
522
+ enumerable: true,
523
+ get: function() {
524
+ return monthAnchorDate;
525
+ }
526
+ });
527
+ Object.defineProperty(exports, "nextUniqueItemId", {
528
+ enumerable: true,
529
+ get: function() {
530
+ return nextUniqueItemId;
531
+ }
532
+ });
533
+ Object.defineProperty(exports, "parseIsoDate", {
534
+ enumerable: true,
535
+ get: function() {
536
+ return parseIsoDate;
537
+ }
538
+ });
539
+ Object.defineProperty(exports, "parseIsoDateTime", {
540
+ enumerable: true,
541
+ get: function() {
542
+ return parseIsoDateTime;
543
+ }
544
+ });
545
+ Object.defineProperty(exports, "parseTimeRange", {
546
+ enumerable: true,
547
+ get: function() {
548
+ return parseTimeRange;
549
+ }
550
+ });
551
+ Object.defineProperty(exports, "recordSpan", {
552
+ enumerable: true,
553
+ get: function() {
554
+ return recordSpan;
555
+ }
556
+ });
557
+ Object.defineProperty(exports, "spanCoversDay", {
558
+ enumerable: true,
559
+ get: function() {
560
+ return spanCoversDay;
561
+ }
562
+ });
563
+ Object.defineProperty(exports, "storageKindFor", {
564
+ enumerable: true,
565
+ get: function() {
566
+ return storageKindFor;
567
+ }
568
+ });
569
+ Object.defineProperty(exports, "ymdKey", {
570
+ enumerable: true,
571
+ get: function() {
572
+ return ymdKey;
573
+ }
574
+ });
575
+
576
+ //# sourceMappingURL=calendarGrid-CaS9er8i.cjs.map