@mulmoclaude/core 0.13.1 → 0.15.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 (59) hide show
  1. package/assets/helps/billing-clients-worklog.md +15 -12
  2. package/assets/helps/collection-skills.md +67 -22
  3. package/assets/helps/todo-collection.md +13 -7
  4. package/dist/{deriveAll-BQ_p5HSB.cjs → calendarGrid-CaR-q36Y.js} +352 -74
  5. package/dist/calendarGrid-CaR-q36Y.js.map +1 -0
  6. package/dist/calendarGrid-gN4sei7s.cjs +1070 -0
  7. package/dist/calendarGrid-gN4sei7s.cjs.map +1 -0
  8. package/dist/collection/core/backlinks.d.ts +20 -0
  9. package/dist/collection/core/ids.d.ts +12 -0
  10. package/dist/collection/core/recordZ.d.ts +23 -0
  11. package/dist/collection/core/schema.d.ts +72 -418
  12. package/dist/collection/core/schemaZ.d.ts +878 -0
  13. package/dist/collection/core/templatePath.d.ts +34 -0
  14. package/dist/collection/core/uiTypes.d.ts +22 -0
  15. package/dist/collection/core/where.d.ts +9 -15
  16. package/dist/collection/index.cjs +50 -330
  17. package/dist/collection/index.cjs.map +1 -1
  18. package/dist/collection/index.d.ts +2 -0
  19. package/dist/collection/index.js +14 -301
  20. package/dist/collection/index.js.map +1 -1
  21. package/dist/collection/paths.cjs +1 -1
  22. package/dist/collection/paths.cjs.map +1 -1
  23. package/dist/collection/paths.js +1 -1
  24. package/dist/collection/paths.js.map +1 -1
  25. package/dist/collection/registry/server/index.cjs +1 -1
  26. package/dist/collection/registry/server/index.js +1 -1
  27. package/dist/collection/server/discovery.d.ts +2 -244
  28. package/dist/collection/server/index.cjs +8 -2
  29. package/dist/collection/server/index.d.ts +1 -0
  30. package/dist/collection/server/index.js +3 -2
  31. package/dist/collection/server/io.d.ts +9 -0
  32. package/dist/collection/server/ontology.d.ts +35 -0
  33. package/dist/collection/server/templatePath.d.ts +1 -34
  34. package/dist/collection/server/validate.d.ts +11 -7
  35. package/dist/collection-watchers/index.cjs +5 -4
  36. package/dist/collection-watchers/index.cjs.map +1 -1
  37. package/dist/collection-watchers/index.js +3 -2
  38. package/dist/collection-watchers/index.js.map +1 -1
  39. package/dist/feeds/index.cjs +4 -4
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/ingestTypes.d.ts +15 -40
  42. package/dist/feeds/server/index.cjs +5 -5
  43. package/dist/feeds/server/index.js +3 -3
  44. package/dist/{ingestTypes-C7EheYZX.cjs → ingestTypes-DG8RExWt.cjs} +3 -3
  45. package/dist/ingestTypes-DG8RExWt.cjs.map +1 -0
  46. package/dist/{ingestTypes-CmJeOUJc.js → ingestTypes-V4upKeBx.js} +2 -2
  47. package/dist/ingestTypes-V4upKeBx.js.map +1 -0
  48. package/dist/{server-DKXXFTbw.js → server-BNTR7ySK.js} +531 -196
  49. package/dist/server-BNTR7ySK.js.map +1 -0
  50. package/dist/{server-G6GtOdaW.cjs → server-bsNrf2yW.cjs} +572 -213
  51. package/dist/server-bsNrf2yW.cjs.map +1 -0
  52. package/package.json +1 -1
  53. package/dist/deriveAll-BQ_p5HSB.cjs.map +0 -1
  54. package/dist/deriveAll-CMFXDQ_G.js +0 -506
  55. package/dist/deriveAll-CMFXDQ_G.js.map +0 -1
  56. package/dist/ingestTypes-C7EheYZX.cjs.map +0 -1
  57. package/dist/ingestTypes-CmJeOUJc.js.map +0 -1
  58. package/dist/server-DKXXFTbw.js.map +0 -1
  59. package/dist/server-G6GtOdaW.cjs.map +0 -1
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A safe skill-relative path: non-empty, no backslash, not absolute,
3
+ * and every `/`-separated segment is a plain `[A-Za-z0-9._-]+` token
4
+ * that isn't `.` / `..` (no traversal). Multi-segment (nested) paths
5
+ * are allowed. The reader's realpath containment is the hard
6
+ * guarantee; this fails a bad path fast.
7
+ */
8
+ export declare function isSafeTemplatePath(value: string): boolean;
9
+ /**
10
+ * An action `template` value: a safe path that lives under the skill's
11
+ * `templates/` subdir. This is the canonical contract — the schema
12
+ * validator rejects anything else up front, and the bridge mirrors
13
+ * exactly these. Nested paths (`templates/mail/welcome.md`) and any
14
+ * extension are allowed as long as the path is otherwise safe.
15
+ */
16
+ export declare function isSafeActionTemplatePath(value: string): boolean;
17
+ /**
18
+ * A custom-view `file` value: a safe path under the skill's `views/`
19
+ * subdir that ends in `.html`. Custom views are LLM-authored HTML the
20
+ * host renders in a sandboxed iframe; constraining them to `views/*.html`
21
+ * keeps the data folder and the schema/template files off-limits to the
22
+ * view-file reader. Same per-segment safety as templates; the reader's
23
+ * realpath containment is the hard guarantee.
24
+ */
25
+ export declare function isSafeCustomViewPath(value: string): boolean;
26
+ /**
27
+ * A custom-view `i18n` value: the JSON dictionary file authors ship next to
28
+ * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the
29
+ * field can only point at a translation file co-located with the view it
30
+ * translates — the same `views/` containment the HTML reader uses. The
31
+ * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able
32
+ * for both authors and tooling.
33
+ */
34
+ export declare function isSafeCustomViewI18nPath(value: string): boolean;
@@ -74,6 +74,28 @@ export interface EmbedView {
74
74
  /** The fixed record id the embed points at, for the message. */
75
75
  recordId: string;
76
76
  }
77
+ /** One column of the backlinks sub-table: a `display` key, labelled from
78
+ * the SOURCE schema (raw key when the source doesn't declare it). */
79
+ export interface BacklinksColumn {
80
+ key: string;
81
+ label: string;
82
+ }
83
+ /** One matching source record: its id (the row links to
84
+ * `/collections/<from>?selected=<id>`) + a pre-formatted cell per column. */
85
+ export interface BacklinksRow {
86
+ id: string;
87
+ /** Cell display strings, aligned with the view's `columns`. */
88
+ cells: string[];
89
+ }
90
+ export interface BacklinksView {
91
+ /** False when the source collection couldn't be loaded — the renderer
92
+ * fails soft to the same empty-state as "no matching rows". */
93
+ found: boolean;
94
+ columns: BacklinksColumn[];
95
+ rows: BacklinksRow[];
96
+ /** Source collection slug (the field's `from`), for the row links. */
97
+ fromSlug: string;
98
+ }
77
99
  /** Active-notification severity for a record, used to accent flagged cards
78
100
  * (kanban left-stripe, etc.). The host computes these from its notifier and
79
101
  * passes them in; this is the structural type the view layer accepts. The host's
@@ -1,28 +1,22 @@
1
- /** Comparison operators one `WhereCond` may apply to `record[field]`. */
2
- export type WhereOp = "eq" | "ne" | "in" | "gt" | "gte" | "lt" | "lte" | "contains";
1
+ import { z } from 'zod';
2
+ import { ValueRefZ, WhereCondZ, WhereZ } from './schemaZ';
3
3
  /** Reads the comparison value from a field instead of a schema literal:
4
4
  * - `record` set → another record: `recordsById[record][field]` (e.g. a
5
5
  * `_config` singleton's `defaultCity`, following a per-user setting);
6
6
  * - `record` omitted → the SAME record being matched (field-to-field, e.g.
7
7
  * `spent > budget`). */
8
- export interface ValueRef {
9
- record?: string;
10
- field: string;
11
- }
8
+ export type ValueRef = z.infer<typeof ValueRefZ>;
12
9
  /** One typed condition: `record[field] <op> value`. The comparison value is
13
10
  * either a literal `value` (a plain string for every op except `in`, which
14
11
  * takes the allowed set) or a `valueFrom` reference resolved against the
15
12
  * `recordsById` map passed to `matchesWhere`. Exactly one of the two is
16
- * expected — enforced by zod at the schema boundary (`server/discovery.ts`),
17
- * not here. */
18
- export interface WhereCond {
19
- field: string;
20
- op: WhereOp;
21
- value?: string | string[];
22
- valueFrom?: ValueRef;
23
- }
13
+ * expected — enforced by zod at the schema boundary (`./schemaZ`), not
14
+ * here. */
15
+ export type WhereCond = z.infer<typeof WhereCondZ>;
16
+ /** Comparison operators one `WhereCond` may apply to `record[field]`. */
17
+ export type WhereOp = WhereCond["op"];
24
18
  /** A `where` clause is the AND of its conditions — every one must match. */
25
- export type Where = WhereCond[];
19
+ export type Where = z.infer<typeof WhereZ>;
26
20
  /** True when `record` satisfies every condition in `where` (AND). An empty
27
21
  * `where` matches everything. `recordsById` — the source collection's
28
22
  * records keyed by primaryKey — resolves any `valueFrom` reference;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_deriveAll = require("../deriveAll-BQ_p5HSB.cjs");
2
+ const require_calendarGrid = require("../calendarGrid-gN4sei7s.cjs");
3
3
  const require_errorMessage = require("../errorMessage--LvXe_Jx.cjs");
4
4
  //#region src/collection/core/presentCollection.ts
5
5
  var TOOL_NAME = "presentCollection";
@@ -149,35 +149,8 @@ function resolveEnumColor(schema, fieldKey, value) {
149
149
  if (rank < 0) return ENUM_NEUTRAL;
150
150
  return rank === 0 ? ENUM_ALERT : ENUM_NUDGE;
151
151
  }
152
- return enumColorClasses(enumValueIndex(schema.fields[fieldKey]?.values, value));
153
- }
154
- //#endregion
155
- //#region src/collection/core/actionVisible.ts
156
- /** Core matcher:
157
- * - no `when` ⇒ always true (visible);
158
- * - otherwise true only when `record[when.field]` is present and its
159
- * stringified value is one of `when.in`.
160
- * A missing/undefined/null field is treated as "not a match"
161
- * (hidden), so a status-gated target never shows on a record that
162
- * lacks the status. */
163
- function whenMatches(when, record) {
164
- if (!when) return true;
165
- const value = record[when.field];
166
- if (value === void 0 || value === null) return false;
167
- return when.in.includes(String(value));
168
- }
169
- /** True when the action's button should render against `record`
170
- * (see whenMatches). */
171
- function actionVisible(action, record) {
172
- return whenMatches(action.when, record);
173
- }
174
- /** True when the field should render against `record`. A field with
175
- * no `when` is always shown; otherwise it's shown only when the
176
- * record matches (e.g. hide a rating field until `visited` is true).
177
- * Purely presentational — a hidden field's stored value is never
178
- * altered, so toggling the gate back on restores it. */
179
- function fieldVisible(field, record) {
180
- return whenMatches(field.when, record);
152
+ const spec = schema.fields[fieldKey];
153
+ return enumColorClasses(enumValueIndex(spec?.type === "enum" ? spec.values : void 0, value));
181
154
  }
182
155
  //#endregion
183
156
  //#region src/collection/core/draft.ts
@@ -252,7 +225,7 @@ function rowDraftToRecord(rowDraft, subFields) {
252
225
  function draftToRecord(state, schema) {
253
226
  const record = {};
254
227
  for (const [key, field] of Object.entries(schema.fields)) {
255
- if (field.type === "derived" || field.type === "embed" || field.type === "toggle") continue;
228
+ if (field.type === "derived" || field.type === "embed" || field.type === "backlinks" || field.type === "toggle") continue;
256
229
  if (field.type === "boolean") {
257
230
  if (shouldEmitBoolean(state, key, field)) record[key] = state.bool[key] === true;
258
231
  continue;
@@ -294,7 +267,7 @@ function isMissingDraftValue(value) {
294
267
  }
295
268
  /** Label of the first required table sub-field empty in any row, else null. */
296
269
  function firstMissingTableSubField(field, rows) {
297
- if (!field.of || !rows) return null;
270
+ if (field.type !== "table" || !rows) return null;
298
271
  for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
299
272
  const row = rows[rowIdx];
300
273
  for (const [subKey, subField] of Object.entries(field.of)) {
@@ -304,8 +277,13 @@ function firstMissingTableSubField(field, rows) {
304
277
  }
305
278
  return null;
306
279
  }
280
+ /** Field types the required check never flags: booleans always hold a
281
+ * value, and the computed/projected kinds (COMPUTED_TYPES — the shared
282
+ * set, so a future computed type can't drift past this check) have no
283
+ * draft input at all. */
284
+ var NEVER_MISSING_TYPES = /* @__PURE__ */ new Set(["boolean", ...require_calendarGrid.COMPUTED_TYPES]);
307
285
  function validateOneField(key, field, draft, record) {
308
- if (!fieldVisible(field, record)) return null;
286
+ if (!require_calendarGrid.fieldVisible(field, record)) return null;
309
287
  if (field.type === "table" && field.of) {
310
288
  const rows = draft.table[key];
311
289
  if (field.required && (!rows || rows.length === 0)) return field.label;
@@ -313,7 +291,7 @@ function validateOneField(key, field, draft, record) {
313
291
  }
314
292
  if (!field.required) return null;
315
293
  if (draft.mode === "create" && field.primary === true) return null;
316
- if (field.type === "boolean" || field.type === "derived" || field.type === "embed" || field.type === "toggle") return null;
294
+ if (NEVER_MISSING_TYPES.has(field.type)) return null;
317
295
  return isMissingDraftValue(draft.text[key]) ? field.label : null;
318
296
  }
319
297
  /** Human-readable label of the first missing required field, or null. */
@@ -334,7 +312,8 @@ var NON_SORTABLE = /* @__PURE__ */ new Set([
334
312
  "table",
335
313
  "image",
336
314
  "file",
337
- "embed"
315
+ "embed",
316
+ "backlinks"
338
317
  ]);
339
318
  function isSortableField(field) {
340
319
  return !NON_SORTABLE.has(field.type);
@@ -444,272 +423,6 @@ function itemLabelOf(item, schema, labelField) {
444
423
  return itemIdOf(item, schema);
445
424
  }
446
425
  //#endregion
447
- //#region src/collection/core/calendarGrid.ts
448
- var MS_PER_DAY = 864e5;
449
- var ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
450
- var TWO_DIGIT_RE = /^\d{2}$/;
451
- var CLOCK_RE = /(\d{1,2}):(\d{2})/g;
452
- var RANGE_SEP_RE = /[-–—~〜~]/;
453
- /** Minutes in a full day — the timeline's vertical extent. */
454
- var MINUTES_PER_DAY = 1440;
455
- function pad2(value) {
456
- return String(value).padStart(2, "0");
457
- }
458
- /** Canonical `YYYY-MM-DD` string for a civil date. */
459
- function ymdKey(ymd) {
460
- return `${String(ymd.year).padStart(4, "0")}-${pad2(ymd.month)}-${pad2(ymd.day)}`;
461
- }
462
- /** Strictly parse a `YYYY-MM-DD` string into a civil date, rejecting
463
- * anything that isn't a real calendar day (e.g. `2026-02-30`, `2026-13-01`).
464
- * Returns null for non-strings and malformed values so callers can route
465
- * records with no usable date into the "no date" tray rather than crash. */
466
- function parseIsoDate(value) {
467
- if (typeof value !== "string") return null;
468
- const match = ISO_DATE_RE.exec(value.trim());
469
- if (!match) return null;
470
- const year = Number(match[1]);
471
- const month = Number(match[2]);
472
- const day = Number(match[3]);
473
- const probe = new Date(Date.UTC(year, month - 1, day));
474
- if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return null;
475
- return {
476
- year,
477
- month,
478
- day
479
- };
480
- }
481
- /** Minutes-of-day for an `HH:MM` pair, or null when out of range. */
482
- function clockToMinutes(hours, minutes) {
483
- if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
484
- return hours * 60 + minutes;
485
- }
486
- /** Strictly parse a `YYYY-MM-DDTHH:MM` (optional `:SS`) datetime into its
487
- * civil date and minutes-of-day. Returns null for anything that isn't a real
488
- * calendar day or a valid 24h clock. */
489
- function parseIsoDateTime(value) {
490
- if (typeof value !== "string") return null;
491
- const trimmed = value.trim();
492
- const tIndex = trimmed.indexOf("T");
493
- if (tIndex === -1) return null;
494
- const ymd = parseIsoDate(trimmed.slice(0, tIndex));
495
- if (!ymd) return null;
496
- const parts = trimmed.slice(tIndex + 1).split(":");
497
- if (parts.length < 2 || parts.length > 3 || !parts.every((part) => TWO_DIGIT_RE.test(part))) return null;
498
- const minutes = clockToMinutes(Number(parts[0]), Number(parts[1]));
499
- if (minutes === null) return null;
500
- return {
501
- ymd,
502
- minutes
503
- };
504
- }
505
- /** Civil date from either a `YYYY-MM-DD` or a `YYYY-MM-DDTHH:MM` value, so the
506
- * month grid buckets date-only and datetime anchors alike. */
507
- function dateOf(value) {
508
- return parseIsoDate(value) ?? parseIsoDateTime(value)?.ymd ?? null;
509
- }
510
- /** Minutes-of-day from a datetime value, or null for date-only / invalid. */
511
- function timeOf(value) {
512
- return parseIsoDateTime(value)?.minutes ?? null;
513
- }
514
- /** Parse a free-form time-string field into start/end minutes-of-day.
515
- * Handles the common shapes in user data:
516
- * "14:00-17:00" → { start: 840, end: 1020 } (range → block)
517
- * "17:00-" → { start: 1020, end: null } (open end → single line)
518
- * "16:30" → { start: 990, end: null } (point in time → single line)
519
- * "終日" / "" → null (no clock → all-day)
520
- * Returns null when no clock token is parseable. */
521
- function parseTimeRange(value) {
522
- if (typeof value !== "string") return null;
523
- const text = value.trim();
524
- if (!text) return null;
525
- const tokens = [...text.matchAll(CLOCK_RE)];
526
- if (tokens.length === 0) return null;
527
- const minutesOf = (match) => clockToMinutes(Number(match[1]), Number(match[2]));
528
- if (!RANGE_SEP_RE.test(text)) {
529
- const startMin = minutesOf(tokens[0]);
530
- return startMin === null ? null : {
531
- startMin,
532
- endMin: null
533
- };
534
- }
535
- const sepIndex = text.search(RANGE_SEP_RE);
536
- let startMin = null;
537
- let endMin = null;
538
- for (const token of tokens) if ((token.index ?? 0) < sepIndex) startMin = minutesOf(token);
539
- else endMin = minutesOf(token);
540
- if (startMin === null) return null;
541
- return {
542
- startMin,
543
- endMin
544
- };
545
- }
546
- function ymdToUtcMs(ymd) {
547
- return Date.UTC(ymd.year, ymd.month - 1, ymd.day);
548
- }
549
- function utcMsToYmd(epochMs) {
550
- const date = new Date(epochMs);
551
- return {
552
- year: date.getUTCFullYear(),
553
- month: date.getUTCMonth() + 1,
554
- day: date.getUTCDate()
555
- };
556
- }
557
- /** Chronological comparison: negative if `left` precedes `right`, 0 if the
558
- * same day, positive if after. */
559
- function compareYmd(left, right) {
560
- return ymdToUtcMs(left) - ymdToUtcMs(right);
561
- }
562
- /** True iff `day` falls within the inclusive span `[span.start, span.end]`. */
563
- function spanCoversDay(span, day) {
564
- return compareYmd(span.start, day) <= 0 && compareYmd(day, span.end) <= 0;
565
- }
566
- /** Build the 6×7 (42-cell) grid for the given month, including the
567
- * leading/trailing days of the adjacent months so every week is full.
568
- * `month` is 1-12. `weekStartsOn` is 0 (Sunday) … 6 (Saturday). */
569
- function buildMonthGrid(year, month, weekStartsOn = 0) {
570
- const lead = (new Date(Date.UTC(year, month - 1, 1)).getUTCDay() - weekStartsOn + 7) % 7;
571
- const startMs = Date.UTC(year, month - 1, 1) - lead * MS_PER_DAY;
572
- const cells = [];
573
- for (let i = 0; i < 42; i++) {
574
- const ymd = utcMsToYmd(startMs + i * MS_PER_DAY);
575
- cells.push({
576
- ymd,
577
- inMonth: ymd.year === year && ymd.month === month,
578
- key: ymdKey(ymd)
579
- });
580
- }
581
- return cells;
582
- }
583
- /** Resolve a record's calendar span from its date/datetime fields. Returns
584
- * null when the anchor date is missing/invalid (→ the caller's "no date"
585
- * tray). An end date that is missing, invalid, or earlier than the start
586
- * collapses to a single-day span — never an inverted range.
587
- *
588
- * Times for the day (time-allocation) view come from, in priority order:
589
- * 1. the clock on a `datetime` anchor/end value, else
590
- * 2. `timeField` — a separate free-form time-string column (e.g. "14:00-17:00").
591
- * A record with no resolvable clock has `startMin === endMin === null`. */
592
- function recordSpan(item, anchorField, endField, timeField) {
593
- const startRaw = item[anchorField];
594
- const start = dateOf(startRaw);
595
- if (!start) return null;
596
- let end = start;
597
- let startMin = timeOf(startRaw);
598
- let endMin = null;
599
- if (endField) {
600
- const endRaw = item[endField];
601
- const parsedEnd = dateOf(endRaw);
602
- if (parsedEnd && compareYmd(parsedEnd, start) >= 0) {
603
- end = parsedEnd;
604
- endMin = timeOf(endRaw);
605
- }
606
- }
607
- if (timeField && startMin === null && endMin === null) {
608
- const range = parseTimeRange(item[timeField]);
609
- if (range) ({startMin, endMin} = range);
610
- }
611
- return {
612
- item,
613
- start,
614
- end,
615
- startMin,
616
- endMin
617
- };
618
- }
619
- /** Split records into those that land on the calendar (with their spans)
620
- * and those with no usable anchor date (the "no date" tray). Spans are
621
- * sorted by start day so same-day stacking is stable across renders. */
622
- function bucketRecords(items, anchorField, endField, timeField) {
623
- const spans = [];
624
- const noDate = [];
625
- for (const item of items) {
626
- const span = recordSpan(item, anchorField, endField, timeField);
627
- if (span) spans.push(span);
628
- else noDate.push(item);
629
- }
630
- spans.sort((left, right) => compareYmd(left.start, right.start));
631
- return {
632
- spans,
633
- noDate
634
- };
635
- }
636
- /** Project a record's span onto a single day for the time-allocation view, or
637
- * null when the span doesn't cover that day. */
638
- function daySlice(span, day) {
639
- if (!spanCoversDay(span, day)) return null;
640
- const hasStart = span.startMin !== null;
641
- const hasEnd = span.endMin !== null;
642
- if (!hasStart && !hasEnd) return {
643
- kind: "allDay",
644
- startMin: 0,
645
- endMin: MINUTES_PER_DAY,
646
- bleedsBefore: false,
647
- bleedsAfter: false
648
- };
649
- const singleDay = compareYmd(span.start, span.end) === 0;
650
- const isStartDay = compareYmd(day, span.start) === 0;
651
- const isEndDay = compareYmd(day, span.end) === 0;
652
- if (singleDay && hasStart && !hasEnd) return {
653
- kind: "line",
654
- startMin: span.startMin,
655
- endMin: span.startMin,
656
- bleedsBefore: false,
657
- bleedsAfter: false
658
- };
659
- const startMin = isStartDay && hasStart ? span.startMin : 0;
660
- const endMin = isEndDay && hasEnd ? span.endMin : MINUTES_PER_DAY;
661
- if (singleDay && endMin <= startMin) return {
662
- kind: "line",
663
- startMin,
664
- endMin: startMin,
665
- bleedsBefore: false,
666
- bleedsAfter: false
667
- };
668
- return {
669
- kind: "block",
670
- startMin,
671
- endMin,
672
- bleedsBefore: !isStartDay,
673
- bleedsAfter: !isEndDay
674
- };
675
- }
676
- function assignLanes(blocks) {
677
- const order = [...blocks.keys()].sort((left, right) => blocks[left].startMin - blocks[right].startMin || blocks[left].endMin - blocks[right].endMin);
678
- const result = blocks.map(() => ({
679
- lane: 0,
680
- lanes: 1
681
- }));
682
- let cluster = [];
683
- let clusterEnd = Number.NEGATIVE_INFINITY;
684
- const laneEnds = [];
685
- const flush = () => {
686
- for (const index of cluster) result[index].lanes = laneEnds.length;
687
- cluster = [];
688
- laneEnds.length = 0;
689
- clusterEnd = Number.NEGATIVE_INFINITY;
690
- };
691
- for (const index of order) {
692
- const block = blocks[index];
693
- if (cluster.length > 0 && block.startMin >= clusterEnd) flush();
694
- let lane = laneEnds.findIndex((end) => end <= block.startMin);
695
- if (lane === -1) {
696
- lane = laneEnds.length;
697
- laneEnds.push(block.endMin);
698
- } else laneEnds[lane] = block.endMin;
699
- result[index].lane = lane;
700
- cluster.push(index);
701
- clusterEnd = Math.max(clusterEnd, block.endMin);
702
- }
703
- flush();
704
- return result;
705
- }
706
- /** Month label key inputs — returns the 1st of the month as a `Date` so the
707
- * component can feed it to `Intl.DateTimeFormat(locale, …)` for a localized
708
- * "April 2026" header without this module taking a locale dependency. */
709
- function monthAnchorDate(year, month) {
710
- return new Date(Date.UTC(year, month - 1, 1));
711
- }
712
- //#endregion
713
426
  //#region src/collection/core/shortHexId.ts
714
427
  /**
715
428
  * 8-char hex id — short, slug-safe, and editable. Produces the same id *shape*
@@ -729,64 +442,71 @@ function defangForPrompt(value) {
729
442
  return value.replace(/[<>]/g, "").replace(/`/g, "'").replace(/\$\{/g, "$ {").replace(/\s+/g, " ").slice(0, DEFANG_MAX_LEN);
730
443
  }
731
444
  //#endregion
732
- exports.AGENT_INGEST_KIND = require_deriveAll.AGENT_INGEST_KIND;
445
+ exports.AGENT_INGEST_KIND = require_calendarGrid.AGENT_INGEST_KIND;
446
+ exports.COMPUTED_TYPES = require_calendarGrid.COMPUTED_TYPES;
733
447
  exports.ENUM_ALERT = ENUM_ALERT;
734
448
  exports.ENUM_NEUTRAL = ENUM_NEUTRAL;
735
449
  exports.ENUM_NUDGE = ENUM_NUDGE;
736
- exports.FEED_SCHEDULES = require_deriveAll.FEED_SCHEDULES;
737
- exports.INGEST_KINDS = require_deriveAll.INGEST_KINDS;
738
- exports.MINUTES_PER_DAY = MINUTES_PER_DAY;
450
+ exports.FEED_SCHEDULES = require_calendarGrid.FEED_SCHEDULES;
451
+ exports.INGEST_KINDS = require_calendarGrid.INGEST_KINDS;
452
+ exports.MINUTES_PER_DAY = require_calendarGrid.MINUTES_PER_DAY;
453
+ exports.SAFE_RECORD_ID_PATTERN = require_calendarGrid.SAFE_RECORD_ID_PATTERN;
454
+ exports.SAFE_SLUG_PATTERN = require_calendarGrid.SAFE_SLUG_PATTERN;
739
455
  exports.TOOL_DEFINITION = TOOL_DEFINITION;
740
456
  exports.TOOL_NAME = TOOL_NAME;
741
- exports.actionVisible = actionVisible;
742
- exports.assignLanes = assignLanes;
457
+ exports.actionVisible = require_calendarGrid.actionVisible;
458
+ exports.assignLanes = require_calendarGrid.assignLanes;
459
+ exports.backlinkRows = require_calendarGrid.backlinkRows;
743
460
  exports.boolSortValue = boolSortValue;
744
- exports.bucketRecords = bucketRecords;
745
- exports.buildMonthGrid = buildMonthGrid;
461
+ exports.bucketRecords = require_calendarGrid.bucketRecords;
462
+ exports.buildMonthGrid = require_calendarGrid.buildMonthGrid;
746
463
  exports.buildUpdatedRecord = buildUpdatedRecord;
747
464
  exports.coerceInlineValue = coerceInlineValue;
748
465
  exports.compareSortValues = compareSortValues;
749
- exports.compareYmd = compareYmd;
750
- exports.dateOf = dateOf;
466
+ exports.compareYmd = require_calendarGrid.compareYmd;
467
+ exports.dateOf = require_calendarGrid.dateOf;
751
468
  exports.dateSortValue = dateSortValue;
752
- exports.daySlice = daySlice;
469
+ exports.daySlice = require_calendarGrid.daySlice;
753
470
  exports.defangForPrompt = defangForPrompt;
754
- exports.deriveAll = require_deriveAll.deriveAll;
471
+ exports.deriveAll = require_calendarGrid.deriveAll;
755
472
  exports.draftToRecord = draftToRecord;
756
- exports.embedTargetId = require_deriveAll.embedTargetId;
473
+ exports.embedTargetId = require_calendarGrid.embedTargetId;
757
474
  exports.emptyRow = emptyRow;
758
475
  exports.enumColorClasses = enumColorClasses;
759
476
  exports.enumSortValue = enumSortValue;
760
477
  exports.enumValueIndex = enumValueIndex;
761
478
  exports.errorMessage = require_errorMessage.errorMessage;
762
- exports.evaluateDerived = require_deriveAll.evaluateDerived;
479
+ exports.evaluateDerived = require_calendarGrid.evaluateDerived;
763
480
  exports.executePresentCollection = executePresentCollection;
764
- exports.fieldVisible = fieldVisible;
765
- exports.firstDateField = require_deriveAll.firstDateField;
481
+ exports.fieldVisible = require_calendarGrid.fieldVisible;
482
+ exports.firstDateField = require_calendarGrid.firstDateField;
766
483
  exports.firstMissingRequiredField = firstMissingRequiredField;
767
- exports.isFieldDrivenEvery = require_deriveAll.isFieldDrivenEvery;
484
+ exports.isFieldDrivenEvery = require_calendarGrid.isFieldDrivenEvery;
485
+ exports.isSafeRecordId = require_calendarGrid.isSafeRecordId;
486
+ exports.isSafeSlug = require_calendarGrid.isSafeSlug;
768
487
  exports.isSortableField = isSortableField;
769
488
  exports.itemIdOf = itemIdOf;
770
489
  exports.itemLabelOf = itemLabelOf;
771
490
  exports.labelFieldFor = labelFieldFor;
772
- exports.matchesWhere = require_deriveAll.matchesWhere;
773
- exports.monthAnchorDate = monthAnchorDate;
491
+ exports.matchesWhere = require_calendarGrid.matchesWhere;
492
+ exports.monthAnchorDate = require_calendarGrid.monthAnchorDate;
774
493
  exports.nextSortDirection = nextSortDirection;
775
494
  exports.numericSortValue = numericSortValue;
776
- exports.parseIsoDate = parseIsoDate;
777
- exports.parseIsoDateTime = parseIsoDateTime;
778
- exports.parseTimeRange = parseTimeRange;
779
- exports.recordSpan = recordSpan;
495
+ exports.parseIsoDate = require_calendarGrid.parseIsoDate;
496
+ exports.parseIsoDateTime = require_calendarGrid.parseIsoDateTime;
497
+ exports.parseTimeRange = require_calendarGrid.parseTimeRange;
498
+ exports.projectBacklinkRow = require_calendarGrid.projectBacklinkRow;
499
+ exports.recordSpan = require_calendarGrid.recordSpan;
780
500
  exports.resolveEnumColor = resolveEnumColor;
781
- exports.resolveIcon = require_deriveAll.resolveIcon;
782
- exports.resolveRowRefs = require_deriveAll.resolveRowRefs;
501
+ exports.resolveIcon = require_calendarGrid.resolveIcon;
502
+ exports.resolveRowRefs = require_calendarGrid.resolveRowRefs;
783
503
  exports.rowFromItem = rowFromItem;
784
- exports.selectDynamicRecord = require_deriveAll.selectDynamicRecord;
504
+ exports.selectDynamicRecord = require_calendarGrid.selectDynamicRecord;
785
505
  exports.shortHexId = shortHexId;
786
506
  exports.sortItems = sortItems;
787
- exports.spanCoversDay = spanCoversDay;
507
+ exports.spanCoversDay = require_calendarGrid.spanCoversDay;
788
508
  exports.stringSortValue = stringSortValue;
789
- exports.whenMatches = whenMatches;
790
- exports.ymdKey = ymdKey;
509
+ exports.whenMatches = require_calendarGrid.whenMatches;
510
+ exports.ymdKey = require_calendarGrid.ymdKey;
791
511
 
792
512
  //# sourceMappingURL=index.cjs.map