@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.
- package/assets/helps/billing-clients-worklog.md +15 -12
- package/assets/helps/collection-skills.md +67 -22
- package/assets/helps/todo-collection.md +13 -7
- package/dist/{deriveAll-BQ_p5HSB.cjs → calendarGrid-CaR-q36Y.js} +352 -74
- package/dist/calendarGrid-CaR-q36Y.js.map +1 -0
- package/dist/calendarGrid-gN4sei7s.cjs +1070 -0
- package/dist/calendarGrid-gN4sei7s.cjs.map +1 -0
- package/dist/collection/core/backlinks.d.ts +20 -0
- package/dist/collection/core/ids.d.ts +12 -0
- package/dist/collection/core/recordZ.d.ts +23 -0
- package/dist/collection/core/schema.d.ts +72 -418
- package/dist/collection/core/schemaZ.d.ts +878 -0
- package/dist/collection/core/templatePath.d.ts +34 -0
- package/dist/collection/core/uiTypes.d.ts +22 -0
- package/dist/collection/core/where.d.ts +9 -15
- package/dist/collection/index.cjs +50 -330
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +14 -301
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/paths.cjs +1 -1
- package/dist/collection/paths.cjs.map +1 -1
- package/dist/collection/paths.js +1 -1
- package/dist/collection/paths.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +1 -1
- package/dist/collection/registry/server/index.js +1 -1
- package/dist/collection/server/discovery.d.ts +2 -244
- package/dist/collection/server/index.cjs +8 -2
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +3 -2
- package/dist/collection/server/io.d.ts +9 -0
- package/dist/collection/server/ontology.d.ts +35 -0
- package/dist/collection/server/templatePath.d.ts +1 -34
- package/dist/collection/server/validate.d.ts +11 -7
- package/dist/collection-watchers/index.cjs +5 -4
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +3 -2
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/feeds/index.cjs +4 -4
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/ingestTypes.d.ts +15 -40
- package/dist/feeds/server/index.cjs +5 -5
- package/dist/feeds/server/index.js +3 -3
- package/dist/{ingestTypes-C7EheYZX.cjs → ingestTypes-DG8RExWt.cjs} +3 -3
- package/dist/ingestTypes-DG8RExWt.cjs.map +1 -0
- package/dist/{ingestTypes-CmJeOUJc.js → ingestTypes-V4upKeBx.js} +2 -2
- package/dist/ingestTypes-V4upKeBx.js.map +1 -0
- package/dist/{server-DKXXFTbw.js → server-BNTR7ySK.js} +531 -196
- package/dist/server-BNTR7ySK.js.map +1 -0
- package/dist/{server-G6GtOdaW.cjs → server-bsNrf2yW.cjs} +572 -213
- package/dist/server-bsNrf2yW.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/deriveAll-BQ_p5HSB.cjs.map +0 -1
- package/dist/deriveAll-CMFXDQ_G.js +0 -506
- package/dist/deriveAll-CMFXDQ_G.js.map +0 -1
- package/dist/ingestTypes-C7EheYZX.cjs.map +0 -1
- package/dist/ingestTypes-CmJeOUJc.js.map +0 -1
- package/dist/server-DKXXFTbw.js.map +0 -1
- 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
|
-
|
|
2
|
-
|
|
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
|
|
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 (`
|
|
17
|
-
*
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
737
|
-
exports.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 =
|
|
471
|
+
exports.deriveAll = require_calendarGrid.deriveAll;
|
|
755
472
|
exports.draftToRecord = draftToRecord;
|
|
756
|
-
exports.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 =
|
|
479
|
+
exports.evaluateDerived = require_calendarGrid.evaluateDerived;
|
|
763
480
|
exports.executePresentCollection = executePresentCollection;
|
|
764
|
-
exports.fieldVisible = fieldVisible;
|
|
765
|
-
exports.firstDateField =
|
|
481
|
+
exports.fieldVisible = require_calendarGrid.fieldVisible;
|
|
482
|
+
exports.firstDateField = require_calendarGrid.firstDateField;
|
|
766
483
|
exports.firstMissingRequiredField = firstMissingRequiredField;
|
|
767
|
-
exports.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 =
|
|
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.
|
|
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 =
|
|
782
|
-
exports.resolveRowRefs =
|
|
501
|
+
exports.resolveIcon = require_calendarGrid.resolveIcon;
|
|
502
|
+
exports.resolveRowRefs = require_calendarGrid.resolveRowRefs;
|
|
783
503
|
exports.rowFromItem = rowFromItem;
|
|
784
|
-
exports.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
|