@mulmoclaude/core 5.6.1 → 5.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.
- package/assets/helps/google-calendar-collection.md +8 -3
- package/dist/collection/core/calendarGrid.d.ts +24 -2
- package/dist/collection/index.cjs +5 -2
- package/dist/collection/index.js +3 -3
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/index.cjs +3 -3
- package/dist/collection/server/index.js +3 -3
- package/dist/collection-watchers/index.cjs +4 -4
- package/dist/collection-watchers/index.js +4 -4
- package/dist/{discovery-B79IxEM6.cjs → discovery-CwBXvtyn.cjs} +2 -2
- package/dist/{discovery-B79IxEM6.cjs.map → discovery-CwBXvtyn.cjs.map} +1 -1
- package/dist/{discovery-DIGau7M8.js → discovery-XoYJI2Ot.js} +2 -2
- package/dist/{discovery-DIGau7M8.js.map → discovery-XoYJI2Ot.js.map} +1 -1
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +4 -4
- package/dist/feeds/server/index.js +4 -4
- package/dist/google/index.cjs +2 -2
- package/dist/google/index.js +2 -2
- package/dist/{ingestTypes-GkG7bJdB.js → ingestTypes-Bs31Xt5c.js} +2 -2
- package/dist/{ingestTypes-GkG7bJdB.js.map → ingestTypes-Bs31Xt5c.js.map} +1 -1
- package/dist/{ingestTypes-DsVH9BYy.cjs → ingestTypes-DS9HiR2I.cjs} +2 -2
- package/dist/{ingestTypes-DsVH9BYy.cjs.map → ingestTypes-DS9HiR2I.cjs.map} +1 -1
- package/dist/{itemId-4P9S_ehz.js → itemId-C9s-Jd5s.js} +58 -23
- package/dist/{itemId-4P9S_ehz.js.map → itemId-C9s-Jd5s.js.map} +1 -1
- package/dist/{itemId-CGT2J7YK.cjs → itemId-CVrNibwT.cjs} +75 -22
- package/dist/{itemId-CGT2J7YK.cjs.map → itemId-CVrNibwT.cjs.map} +1 -1
- package/dist/{promptSafety-BpBTh5xO.js → promptSafety-B5uGroik.js} +2 -2
- package/dist/{promptSafety-BpBTh5xO.js.map → promptSafety-B5uGroik.js.map} +1 -1
- package/dist/{promptSafety-caJWd1ZQ.cjs → promptSafety-BGV1wrE6.cjs} +2 -2
- package/dist/{promptSafety-caJWd1ZQ.cjs.map → promptSafety-BGV1wrE6.cjs.map} +1 -1
- package/dist/{server--34TYvGP.js → server-CM5XNicg.js} +4 -4
- package/dist/{server--34TYvGP.js.map → server-CM5XNicg.js.map} +1 -1
- package/dist/{server-DZdM2cLD.cjs → server-ObUrsDs2.cjs} +4 -4
- package/dist/{server-DZdM2cLD.cjs.map → server-ObUrsDs2.cjs.map} +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"itemId-4P9S_ehz.js","names":[],"sources":["../src/collection/core/fieldText.ts","../src/collection/core/schema.ts","../src/collection/core/ids.ts","../src/collection/core/collectionKey.ts","../src/collection/core/serverTime.ts","../src/collection/core/accentColor.ts","../src/collection/core/calendarGrid.ts","../src/collection/core/itemId.ts"],"sourcesContent":["// Turning a record field into text.\n//\n// `CollectionItem` is `Record<string, unknown>`, so a field holds whatever the\n// record's JSON had — including arrays and objects (real workspace data has\n// plenty: weather `hourly`, GeoJSON `geometry`, `sites` lists). Bare\n// `String(value)` on one of those yields `\"[object Object]\"`, which then gets\n// compared, matched or displayed as if it were a value. Nothing throws; a\n// predicate just silently stops matching, or the UI shows `[object Object]`.\n//\n// These two helpers are the only sanctioned way to read a field as text. The\n// rule they encode was already in `itemLabelOf`: accept primitives, let\n// everything else fall through to the caller's fallback.\n\n/** A field value that has a meaningful text form. Dates are included because\n * a JSON record can carry one once it has been revived. */\nconst isTextable = (value: unknown): value is string | number | boolean | Date =>\n typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\" || value instanceof Date;\n\n/** The field's text, or `null` when it has no meaningful one (absent, or an\n * array/object that would stringify to `\"[object Object]\"`).\n *\n * Returning `null` rather than `\"\"` keeps \"the field is empty\" distinct from\n * \"the field can't be text\" — a matcher must not treat an object-valued field\n * as an empty string and match `\"\"`. */\nexport function fieldTextOrNull(value: unknown): string | null {\n if (value === undefined || value === null) return null;\n if (!isTextable(value)) return null;\n if (value instanceof Date) {\n // `new Date(\"nonsense\")` is still `instanceof Date`, and `toISOString()`\n // throws `RangeError` on it. This helper sits on the match, sort and\n // display paths, so one unparseable date in one record would take the whole\n // render down — the loud version of the bug this module exists to prevent.\n return Number.isNaN(value.getTime()) ? null : value.toISOString();\n }\n return String(value);\n}\n\n/** The field's text, or `fallback` (default `\"\"`) when it has none. Use where\n * a string is required and an empty one is a safe stand-in — display, sort\n * keys, CSV cells. Where the distinction matters, use {@link fieldTextOrNull}. */\nexport function fieldText(value: unknown, fallback = \"\"): string {\n return fieldTextOrNull(value) ?? fallback;\n}\n","// Schema-driven collection types. A \"collection\" is a skill (under\n// .claude/skills/<slug>/) that also ships a sibling `schema.json`.\n// The host's <CollectionView> reads the schema + records and renders\n// a table/form; Claude reads SKILL.md and CRUDs the records as JSON\n// files.\n//\n// SINGLE SOURCE OF TRUTH: every type describing the schema.json contract is\n// derived (`z.infer`) from the zod definitions in `./schemaZ` — the shapes,\n// their doc comments, and the validation rules live THERE; this module only\n// re-derives the TypeScript names consumers import. The imports from\n// `./schemaZ` are type-only, so zod never reaches the browser bundle through\n// the isomorphic barrel; at runtime the dependency points the other way\n// (schemaZ imports this module's consts).\n//\n// Field specs are a DISCRIMINATED UNION on `type`: narrow with\n// `field.type === \"enum\"` (etc.) before reading a variant key like `values`,\n// `to`, `formula`, or `of`.\n\nimport { fieldText } from \"./fieldText\";\nimport type { z } from \"zod\";\nimport type {\n ActionSpecZ,\n CollectionSchemaZ,\n CustomViewZ,\n DataSourceZ,\n DynamicIconRuleZ,\n DynamicIconSourceZ,\n DynamicIconSpecZ,\n EveryFieldDrivenZ,\n EveryLiteralZ,\n EveryZ,\n FieldSpecZ,\n IngestZ,\n SpawnZ,\n StorageZ,\n SubFieldSpecZ,\n WhenZ,\n} from \"./schemaZ\";\n\n/** Minimal \"this collection is a feed\" descriptor carried on the schema.\n * Deliberately narrow — the canonical collection contract stays\n * independent of the host's feeds subsystem. The host's richer retrieval\n * spec (`IngestSpec` in `feeds/ingestTypes.ts`) is a subtype, so feed code\n * reads the extra fields by typing feed schemas with that subtype;\n * collection rendering only needs these three + the presence check. */\nexport interface CollectionIngest {\n kind: string;\n schedule: string;\n /** Optional time-of-day anchor for `schedule: \"daily\"` — the hour (0–23) to\n * refresh around (the host ticks hourly, so the run lands within that hour).\n * Ignored for non-daily schedules. Absent ⇒ elapsed-based daily (\"≥24 h since\n * the last run\"). NOTE: **UTC**, not local — compared via `getUTCHours()` for\n * an unambiguous, DST-free check (matching the rest of the scheduler), so\n * convert local times before writing (e.g. 07:00 JST → `atHour: 22`). */\n atHour?: number;\n /** Declarative retrievers (`rss`/`atom`/`http-json`) only — the host fetches\n * this URL on the schedule. Absent for `kind: \"agent\"`, where the agent owns\n * retrieval. */\n url?: string;\n /** `kind: \"agent\"` only: role id the scheduled hidden worker runs in. */\n role?: string;\n /** `kind: \"agent\"` only: skill-relative template path (under `templates/`)\n * whose prose tells the worker how to refresh the records. */\n template?: string;\n}\n\n/** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's\n * feeds engine dispatches on these; they live here (with the schema contract)\n * so the schema validator can enforce them. The host re-exports these from\n * `server/workspace/feeds/ingestTypes.ts`. */\nexport const INGEST_KINDS = [\"rss\", \"atom\", \"http-json\"] as const;\nexport type IngestKind = (typeof INGEST_KINDS)[number];\n\n/** The agent-performed ingest kind. Instead of a declarative fetch, the host\n * dispatches a hidden background chat (origin `system`) in `ingest.role`,\n * seeded with `ingest.template` + a summary of every record, on the\n * `ingest.schedule` cadence; the worker edits records via the collections io\n * layer. Kept separate from {@link INGEST_KINDS} (which the declarative\n * retriever registry keys on) so the schema validator can model `ingest` as a\n * discriminated union without the feeds engine gaining an \"agent\" retriever. */\nexport const AGENT_INGEST_KIND = \"agent\" as const;\nexport type AgentIngestKind = typeof AGENT_INGEST_KIND;\n\n/** Refresh cadences a Feed's `ingest.schedule` may declare. */\nexport const FEED_SCHEDULES = [\"hourly\", \"daily\", \"weekly\", \"on-demand\"] as const;\nexport type FeedSchedule = (typeof FEED_SCHEDULES)[number];\n\n// \"feed\" collections live in the non-skill `<workspace>/feeds/` registry\n// and carry an `ingest` block; they reuse the same storage + rendering\n// as skill-backed collections but are never loaded into the agent prompt.\nexport type CollectionSource = \"user\" | \"project\" | \"feed\";\n\n/** One field of a record — a discriminated union on `type`; see the variant\n * docs in `./schemaZ` (`FieldSpecZ`). */\nexport type CollectionFieldSpec = z.infer<typeof FieldSpecZ>;\n\n/** A `table` field's row sub-schema entry — the field union minus `table` /\n * `derived` / display-only types (see `SubFieldSpecZ`). */\nexport type CollectionSubFieldSpec = z.infer<typeof SubFieldSpecZ>;\n\nexport type CollectionFieldType = CollectionFieldSpec[\"type\"];\n\n/** The computed-boolean variant — a `where` predicate bound to a field\n * name; see `FlagFieldZ`. */\nexport type CollectionFlagField = Extract<CollectionFieldSpec, { type: \"flag\" }>;\n\n/** derived/embed/backlinks/rollup/toggle/flag are host-computed or\n * projected — never written to the record JSON, so required / value\n * checks and edit-draft slots must not apply to them. THE single source\n * for \"computed\" — lives here (zod-free at runtime) so browser code\n * (`./draft`) and the zod record compiler (`./recordZ`, which re-exports\n * it) share one set instead of drifting copies. */\nexport const COMPUTED_TYPES: ReadonlySet<CollectionFieldType> = new Set<CollectionFieldType>([\"derived\", \"embed\", \"backlinks\", \"rollup\", \"toggle\", \"flag\"]);\n\n/** Optional visibility predicate: the target (an action button or a\n * field) renders only when the open record's `field` (stringified) is\n * one of `in`. Generic and domain-free — the host evaluates it against\n * the record with no knowledge of what the field means. Absent ⇒\n * always shown. */\nexport type CollectionWhen = z.infer<typeof WhenZ>;\n\n/** @deprecated Name retained for back-compat; use {@link CollectionWhen}.\n * Both actions and fields share the same predicate shape. No in-repo\n * consumers, but the package is public API (MulmoTerminal). */\n// eslint-disable-next-line sonarjs/redundant-type-aliases -- deliberate deprecated back-compat export\nexport type CollectionActionWhen = CollectionWhen;\n\n/** A schema-declared, per-record action rendered as a button in the\n * read-only detail view. Pure UI/behaviour directive — never stored,\n * never validated against record data. All domain specifics (label,\n * role, template — or the declarative `set`) live in the schema / skill\n * folder, so the host stays generic. A discriminated union on `kind`;\n * see `ActionSpecZ`. */\nexport type CollectionAction = z.infer<typeof ActionSpecZ>;\n\n/** The kind of work an action kicks off: `\"chat\"` (visible LLM chat),\n * `\"agent\"` (hidden LLM worker), or `\"mutate\"` (declarative host write,\n * no LLM). */\nexport type CollectionActionKind = CollectionAction[\"kind\"];\n\n/** The LLM-seeded action variants (`role` + `template`). */\nexport type CollectionSeededAction = Extract<CollectionAction, { kind: \"chat\" | \"agent\" }>;\n\n/** The declarative host-write variant (`set` + optional `require`/`params`). */\nexport type CollectionMutateAction = Extract<CollectionAction, { kind: \"mutate\" }>;\n\n/** A custom (LLM-authored) HTML view for a collection. The host renders\n * `file` in a sandboxed iframe over the collection's records; the view\n * reaches its data only through a slug- and capability-scoped token (see\n * `server/api/auth/viewToken.ts`). Pure data — the host holds no\n * view-specific code; meaning lives in the HTML file + this registration.\n * See `CustomViewZ` for the per-key contracts. */\nexport type CollectionCustomView = z.infer<typeof CustomViewZ>;\n\n/** What a custom view's capability token is allowed to do against the\n * collection's data endpoint. `read` returns enriched records (getItems\n * semantics); `write` validates-and-stores rows (putItems semantics).\n * There is deliberately no `delete` — a view can never do more than the\n * agent's own `manageCollection` tool.\n *\n * TWO HOST INVARIANTS ride on this token, and both are easy to break in a\n * multi-root host:\n *\n * 1. **The token carries an OPAQUE scope, never a path.** A collection's\n * identity is `(root, slug)`, so a token minted in a multi-root host has to\n * say which root — but the token is signed, not encrypted, and is handed to\n * an LLM-authored iframe. An absolute root in the payload publishes the\n * user's home directory to that iframe. Mint an opaque project id and\n * resolve it host-side.\n * 2. **The scope goes in the TOKEN, not on `dataUrl`.** `__MC_VIEW.dataUrl` is\n * a bare base URL that views concatenate onto (`+ \"?fields=…\"`,\n * `+ \"/query\"`, `+ \"/actions/…\"`, `+ \"/image?path=…\"` — see\n * `assets/helps/custom-view.md`). A host that appends its own query\n * parameter there to carry the project breaks every one of those calls. */\nexport type CollectionViewCapability = NonNullable<CollectionCustomView[\"capabilities\"]>[number];\n\n/** How a `spawn` advances the source item's `triggerField` date to\n * produce the successor's. All arithmetic is done on the civil\n * (year, month, day) triple — never by adding milliseconds — so month\n * lengths and leap years are handled correctly. */\nexport type CollectionEvery = z.infer<typeof EveryLiteralZ>;\n\n/** Recurrence unit for a `spawn.every` advance. */\nexport type CollectionRecurUnit = CollectionEvery[\"unit\"];\n\n/** Field-driven recurrence: the advance interval is selected PER RECORD by\n * the value of an `enum` field (`fromField`), looked up in `map`. See\n * `EveryFieldDrivenZ`. */\nexport type CollectionEveryFieldDriven = z.infer<typeof EveryFieldDrivenZ>;\n\n/** The `every` of a `spawn`: either a single literal interval applied to\n * every record, or a per-record interval selected by an `enum` field. The\n * literal arm is what `advanceTriggerDate` consumes — the field-driven arm\n * is resolved down to one of its `map` values before the date math runs. */\nexport type CollectionSpawnEvery = z.infer<typeof EveryZ>;\n\n/** Narrowing guard: true when `every` is the field-driven arm. */\nexport function isFieldDrivenEvery(every: CollectionSpawnEvery): every is CollectionEveryFieldDriven {\n return \"fromField\" in every;\n}\n\n/** Host-driven recurrence. See `SpawnZ`. */\nexport type CollectionSpawn = z.infer<typeof SpawnZ>;\n\n/** One rule in a `dynamicIcon.rules` list: when the resolved source\n * record matches `where` (an AND of typed conditions, see `./where`),\n * the collection's effective launcher icon becomes `icon`. Evaluated top\n * to bottom — the first match wins. */\nexport type DynamicIconRule = z.infer<typeof DynamicIconRuleZ>;\n\n/** Where a {@link DynamicIconSpec}'s source record comes from: a (possibly\n * cross-collection) pool of records, optionally narrowed by `where` and\n * reduced to a single record by `from`. */\nexport type DynamicIconSource = z.infer<typeof DynamicIconSourceZ>;\n\n/** Declarative \"data state → icon\" mapping for a collection's launcher\n * shortcut icon (see `CollectionSchema.dynamicIcon`). When absent, the\n * launcher icon is the static `schema.icon`. */\nexport type DynamicIconSpec = z.infer<typeof DynamicIconSpecZ>;\n\n/** The `ingest` block as the schema validator accepts it — a discriminated\n * union on `kind` (declarative retrievers | agent worker). The feeds\n * subsystem's `IngestSpec` is the same union under its historical name. */\nexport type CollectionIngestSpec = z.infer<typeof IngestZ>;\n\n/** The `dataSource` block: this collection's records are the rows of an\n * external read-only data file (v1: CSV). See `DataSourceZ`. */\nexport type CollectionDataSource = z.infer<typeof DataSourceZ>;\n\n/** The `storage` block: an alternative WRITABLE record backend (v1:\n * sqlite). See `StorageZ`. */\nexport type CollectionStorage = z.infer<typeof StorageZ>;\n\n/** Every storage backend a schema can select. `file` is the implicit\n * default (`dataPath`); `csv` is implied by `dataSource`; other kinds are\n * named explicitly via `storage.type`. The server's store factory registry\n * (`server/store.ts`) is keyed by this. */\nexport type CollectionStorageKind = \"file\" | \"csv\" | \"sqlite\" | \"firestore\";\n\n/** Which storage backend serves this schema's records. Derived, not stored:\n * existing schemas carry no `storage` key and must keep resolving exactly\n * as before (`dataSource` ⇒ csv, else file). */\nexport function storageKindFor(schema: Pick<CollectionSchema, \"dataSource\" | \"storage\">): CollectionStorageKind {\n if (schema.dataSource !== undefined) return \"csv\";\n return schema.storage?.type ?? \"file\";\n}\n\n/** The whole `schema.json` contract. Key-level docs live on\n * `CollectionSchemaZ` in `./schemaZ`. */\nexport type CollectionSchema = z.infer<typeof CollectionSchemaZ>;\n\n/** True when `schema` declares an external `dataSource` — i.e. the\n * collection is READ-ONLY through every UI/tool write path (updates\n * happen by editing/replacing the data file itself). Isomorphic: both\n * the server write guards and the client's control hiding key off this\n * one predicate. */\nexport function isReadOnlySchema(schema: Pick<CollectionSchema, \"dataSource\">): boolean {\n return schema.dataSource !== undefined;\n}\n\nexport interface CollectionSummary {\n slug: string;\n title: string;\n icon: string;\n /** Optional accent colour name (see `ACCENT_COLORS`), drawn as a pale chip\n * behind the launcher glyph. Absent when the schema names none.\n *\n * Declared NARROW (no `| undefined`), like its `appId` / `readonly`\n * siblings: the remote-host handlers pass these shapes through `Jsonify`,\n * which drops `undefined` from a member union, so a widened optional stops\n * being assignable there. Producers therefore ADD the key conditionally\n * rather than assigning `undefined` to it. */\n color?: string;\n source: CollectionSource;\n /** Present (true) when the collection is backed by an external\n * `dataSource` and therefore read-only in every UI/tool write path.\n * Absent-when-writable, matching the other optional summary flags. */\n readonly?: true;\n /** Slugs of the source collection(s) a `dynamicIcon` icon was computed\n * from — present only when `schema.dynamicIcon` is set. Lets a client\n * know which collection change-channel(s) to watch for a live icon\n * update (see `useDynamicShortcutIcons`). */\n iconSources?: string[];\n /** The app a SHARED collection belongs to — present iff the schema declares\n * `storage.type: \"firestore\"`. A client needs it to subscribe to the right\n * live-change channel: a shared collection publishes on\n * `collection:app/<aid>/<cid>`, and a subscriber that keys on the name alone\n * listens to the LOCAL channel, so the refetch never arrives. Not a secret —\n * it is committed in the repository every clone reads. */\n appId?: string;\n}\n\nexport interface CollectionDetail extends CollectionSummary {\n schema: CollectionSchema;\n}\n\nexport type CollectionItem = Record<string, unknown>;\n\n/** Resolve an `embed` field's target record id: the fixed `id`, or the value\n * of the sibling `idField` on this record (empty string when neither applies\n * — the caller renders that as \"no record\"). Pure + isomorphic so the server\n * projection (`derive.ts`) and the client preview (`useCollectionRendering`)\n * resolve embeds identically. Non-`embed` fields resolve to \"no record\". */\nexport function embedTargetId(field: CollectionFieldSpec, record: CollectionItem | null): string {\n if (field.type !== \"embed\") return \"\";\n if (field.id) return field.id;\n if (field.idField && record) return fieldText(record[field.idField]);\n return \"\";\n}\n","// Pure slug / record-id character rules. Shared by the isomorphic schema\n// validator (`./schemaZ`) — which must stay node-free — and the server-side\n// path sanitisers (`../server/paths`), which wrap these patterns with the\n// `path.basename` round-trip CodeQL recognises as a `js/path-injection`\n// sanitiser. Both layers MUST gate on the same patterns; importing them from\n// here is what keeps them in sync.\n\n// The ONE slug pattern — `server/workspace/skills/catalog.ts` imports it\n// for its own sanitiser, so there is no second copy to keep in sync.\n// Bounded character classes, no nested quantifiers; ReDoS-safe.\nimport { fieldText } from \"./fieldText\";\nimport type { CollectionItem } from \"./schema\";\n\n// eslint-disable-next-line security/detect-unsafe-regex -- non-overlapping character classes, no catastrophic backtracking\nexport const SAFE_SLUG_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;\n\n// Record ids are a superset of slugs: they're only ever filename stems\n// (`<id>.json`), never directory names or URL segments, so they may carry\n// dots — natural keys like a Slack ts (`1718900000.123456`), a SemVer\n// (`1.2.3`), or a decimal timestamp. The interior class adds `.` to the slug\n// set; the explicit `..` reject in `isSafeRecordId` keeps a\n// parent-dir-looking segment out while still allowing repeated `-`/`_`\n// (`a--b`, `a__b`). Start/end stay alphanumeric so leading/trailing dots\n// (hidden files, the special `.`/`..` names) and `..`-only ids are all\n// excluded.\n// eslint-disable-next-line security/detect-unsafe-regex -- non-overlapping character classes, no catastrophic backtracking\nexport const SAFE_RECORD_ID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;\n\n/** True when `value` is a well-formed collection slug (alphanumeric /\n * hyphen / underscore, no path separators). The pattern admits no `/`,\n * `\\`, or `.`, so a passing value is trivially also a safe basename —\n * validation callers need no `path.basename` round-trip (path-building\n * callers use `../server/paths#safeSlugName`, which adds it). */\nexport function isSafeSlug(value: string): boolean {\n return typeof value === \"string\" && SAFE_SLUG_PATTERN.test(value);\n}\n\n/** True when `value` is a well-formed record id (slug charset plus interior\n * dots), with any `..` substring rejected explicitly. Validation-only\n * counterpart of `../server/paths#safeRecordId`. */\nexport function isSafeRecordId(value: string): boolean {\n if (typeof value !== \"string\" || !SAFE_RECORD_ID_PATTERN.test(value)) return false;\n return !value.includes(\"..\");\n}\n\nconst DEFAULT_UNIQUE_ID_ATTEMPTS = 8;\n\n/** Pick an id not already in `existing`, re-rolling `generate()` up to\n * `maxAttempts` times before giving up and returning the last candidate.\n * Collisions on a wide id space are astronomically unlikely, so a caller's\n * own overwrite guard is the final backstop rather than an unbounded loop. */\nexport function generateUniqueId(existing: ReadonlySet<string>, generate: () => string, maxAttempts: number = DEFAULT_UNIQUE_ID_ATTEMPTS): string {\n let candidate = generate();\n for (let attempt = 0; attempt < maxAttempts && existing.has(candidate); attempt++) {\n candidate = generate();\n }\n return candidate;\n}\n\n/** An id not already used by any loaded record: collects the in-memory primary\n * keys and re-rolls `generate()` against them (see {@link generateUniqueId}).\n * The pure core of the view's create-form id prefill. */\nexport function nextUniqueItemId(\n items: readonly CollectionItem[],\n primaryKey: string,\n generate: () => string,\n maxAttempts: number = DEFAULT_UNIQUE_ID_ATTEMPTS,\n): string {\n const existing = new Set(items.map((item) => fieldText(item[primaryKey])));\n return generateUniqueId(existing, generate, maxAttempts);\n}\n","// What a collection IS, as a value.\n//\n// Until now a collection's identity was `(root, slug)` — see the INVARIANT on\n// `CollectionHost` — and that is still exactly right for a collection that\n// lives in a directory. A SHARED collection does not: it is published to\n// Firestore under `apps/{aid}/collections/{cid}`, several machines resolve it,\n// and no one of their paths is its name. Keying it by the path it happened to\n// be published from would make the same collection two collections.\n//\n// The two identities COEXIST. A local collection's behaviour must not change at\n// all, so this is a discriminated union rather than a widening of either one:\n// every surface the INVARIANT enumerates — a cache, a pubsub channel, a view\n// token, a notification id, a rendered card — keys on this type, and the\n// compiler then refuses the thing that keeps happening by hand, which is keying\n// on the NAME alone (`slug` / `cid`) and letting two collections that share a\n// name collide.\n//\n// Isomorphic on purpose: a card and a channel name are decided on both sides of\n// the wire, so this module imports nothing from node.\n\nimport { SAFE_SLUG_PATTERN } from \"./ids\";\n\n/** A collection in a directory. `root` must already be canonical\n * (`canonicalRoot`) — it is an identity here, not a path to read, and `/proj`\n * vs `/proj/` would be two collections. Server callers should build these\n * through `localCollectionKey` in `collection/server`, which canonicalises;\n * the arm is spelled out here so the type itself stays isomorphic. */\nexport interface LocalCollectionKey {\n kind: \"local\";\n root: string;\n slug: string;\n}\n\n/** A collection published to a shared app: `apps/{aid}/collections/{cid}`.\n * `aid` is committed in the repository, so every clone resolves the same\n * collection and an invitation is about authorization, never discovery. */\nexport interface SharedCollectionKey {\n kind: \"shared\";\n aid: string;\n cid: string;\n}\n\nexport type CollectionKey = LocalCollectionKey | SharedCollectionKey;\n\nexport const isLocalCollectionKey = (key: CollectionKey): key is LocalCollectionKey => key.kind === \"local\";\nexport const isSharedCollectionKey = (key: CollectionKey): key is SharedCollectionKey => key.kind === \"shared\";\n\n/** Build a local key from an ALREADY-CANONICAL root — and refuse one that is\n * not. See {@link scopePart}. */\nexport const localCollectionKeyOf = (root: string, slug: string): LocalCollectionKey => ({\n kind: \"local\",\n root: scopePart(root, \"root\"),\n slug: namePart(slug, \"slug\"),\n});\n\n/** Build a shared key. */\nexport const sharedCollectionKey = (aid: string, cid: string): SharedCollectionKey => ({\n kind: \"shared\",\n aid: namePart(aid, \"aid\"),\n cid: namePart(cid, \"cid\"),\n});\n\n/** The collection's NAME within its scope: the slug, or the shared `cid`.\n *\n * This is what a schema file, a URL segment and a label are keyed by, and it\n * is deliberately NOT enough to identify a collection — that is the whole\n * point of the union. Use it to look things up INSIDE a known scope, never as\n * a map key across scopes. */\nexport const collectionKeyName = (key: CollectionKey): string => (key.kind === \"local\" ? key.slug : key.cid);\n\n// NUL separates the parts: it cannot occur in a path, a slug, an app id or a\n// collection id, so the encoding is unambiguous and needs no escaping.\nconst SEP = \"\\u0000\";\n\n/** A ROOT: any non-empty string that can be encoded. A path's charset is the\n * filesystem's, so the only thing to require is that it survives the encoding.\n *\n * \"NUL cannot occur in a path\" is true of every real value and is the reason\n * the encoding needs no escaping — but a type whose whole job is to be an\n * identity must not take the claim on trust. Without this, `(\"a\\0b\", \"c\")`\n * and `(\"a\", \"b\\0c\")` encode to the SAME string, so `sameCollectionKey` calls\n * two different collections equal and the id parses back to nothing. Empty is\n * refused for the same reason: it makes the id ambiguous about which part was\n * missing.\n *\n * A throw, where `parseCollectionKeyId` returns null: building a key is code\n * making an identity, and a bad one there is a programming error. Parsing is\n * reading something off a disk or a wire, where an unrecognised entry is a\n * thing to skip. */\nfunction scopePart(value: string, field: string): string {\n if (value.length === 0) throw new Error(`CollectionKey: ${field} must not be empty`);\n if (value.includes(SEP)) throw new Error(`CollectionKey: ${field} must not contain NUL`);\n if (!isCanonicalRootShape(value)) throw new Error(`CollectionKey: ${field} \"${value}\" is not a canonical root`);\n return value;\n}\n\n/** Is this root ALREADY in the shape `canonicalRoot` (path.resolve) produces?\n *\n * This module cannot canonicalise — that needs `node:path` and this file is\n * isomorphic — so it does the other half: it REFUSES anything that is not\n * already canonical. Which is the part that matters, because the failure is\n * silent. `/work/proj/` and `/work/proj` are the same collection, and a key\n * built from the first compares unequal to a key built from the second: two\n * cache entries, two channels, two bells for one collection — exactly the\n * identity split this type exists to remove. A decoder reading ids off a disk\n * is where such a spelling arrives.\n *\n * The properties are `path.resolve`'s own: absolute, no `.` or `..` segment,\n * no doubled separator, no trailing one. The Windows drive form is allowed so\n * a canonical root there is not refused. */\nfunction isCanonicalRootShape(root: string): boolean {\n const windows = /^[A-Za-z]:\\\\/.test(root);\n const sep = windows ? \"\\\\\" : \"/\";\n if (!windows && !root.startsWith(\"/\")) return false;\n if (root.length > 1 && root.endsWith(sep)) return false;\n if (root.includes(sep + sep)) return false;\n return !root.split(sep).some((segment) => segment === \".\" || segment === \"..\");\n}\n\n/** A NAME — a slug, a shared `cid`, or an `aid`: the collection-slug charset,\n * `[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?`.\n *\n * THIS TYPE IS THE SINGLE SOURCE OF TRUTH for what a name may be, and that is\n * the point rather than a nicety. A name is re-encoded by every downstream\n * identity — the completion-bell id (`<scope>\\0<name>:<itemId>`, split at the\n * first colon), a pubsub channel (`collection:app/<aid>/<name>`), a cache key\n * — and each of those has a different character it cannot survive. With the\n * rule stated only downstream, the layers disagree: a cid of `sales:2026`\n * builds fine, then the bell id decodes as a DIFFERENT collection and the\n * channel name throws inside a publisher whose catch swallows it, so the live\n * update simply stops arriving. One rule here makes every encoding safe by\n * construction; the checks downstream stay as belt-and-braces for callers that\n * never went through a key.\n *\n * A local slug is already `safeSlugName`-validated upstream, so this changes\n * nothing for it. A shared `aid` / `cid` had no gate at all — they are\n * Firestore document ids under `apps/{aid}/collections/{cid}` AND the name the\n * same collection has on disk, so the slug charset is what they already are. */\nfunction namePart(value: string, field: string): string {\n if (!isValidCollectionName(value)) throw new Error(`CollectionKey: ${field} \"${value}\" is not a valid collection name`);\n return value;\n}\n\n/** The name rule as a predicate, for the encoders that take RAW strings rather\n * than a key (the completion-bell id, a channel name). They are reachable by a\n * caller that never built a key, and each has its own delimiter to be broken\n * by, so they need to apply the same rule — not a rule of their own, which is\n * how the layers came to disagree in the first place. */\nexport function isValidCollectionName(value: string): boolean {\n return SAFE_SLUG_PATTERN.test(value);\n}\n\n/** A stable string form, for the places that need a primitive key: a Map, a\n * pubsub channel name, a notification id, a card's reconciliation key.\n *\n * Round-trips through {@link parseCollectionKeyId}. The `kind` is written\n * first so a local key and a shared key can never collide however their parts\n * are spelled. */\nexport const collectionKeyId = (key: CollectionKey): string =>\n key.kind === \"local\" ? `local${SEP}${key.root}${SEP}${key.slug}` : `shared${SEP}${key.aid}${SEP}${key.cid}`;\n\n/** Decode a {@link collectionKeyId}, or `null` when the string did not come\n * from one — or carries values no key may hold.\n *\n * Decoding goes THROUGH the constructors. Building the union here directly\n * would let a string off a disk or a wire mint an identity the constructors\n * refuse: `shared\\0salon\\0sales:2026` would become a key whose name the\n * completion-bell id and the pubsub channel cannot represent, and the single\n * source of truth for a name would have a back door.\n *\n * Null rather than a throw, and a throw from a constructor caught here: these\n * strings are read back from storage, where an unrecognised or stale entry is\n * a thing to skip, not a crash.\n *\n * A non-canonical root is refused here too, for the same reason: `/work/proj/`\n * and `/work/proj` name one collection, and two keys that compare unequal are\n * two cache entries, two channels and two bells for it. This module cannot\n * canonicalise (that needs `node:path`), so it rejects instead — see\n * {@link scopePart}. */\nexport function parseCollectionKeyId(encoded: string): CollectionKey | null {\n const parts = encoded.split(SEP);\n if (parts.length !== 3) return null;\n const [kind, first, second] = parts;\n if (kind === undefined || first === undefined || second === undefined) return null;\n try {\n if (kind === \"local\") return localCollectionKeyOf(first, second);\n if (kind === \"shared\") return sharedCollectionKey(first, second);\n } catch {\n return null;\n }\n return null;\n}\n\n/** Do two keys name the same collection? */\nexport const sameCollectionKey = (one: CollectionKey, other: CollectionKey): boolean => collectionKeyId(one) === collectionKeyId(other);\n","// The one shape a server-stamped instant takes, and the codec that keeps it\n// that way.\n//\n// A shared (firestore) collection can pin a field to the SERVER's clock:\n// `public.submit.<cid>.stampField` makes the rules require\n// `request.resource.data[field] == request.time` on create and freeze it\n// afterwards. What is stored is therefore a Firestore `Timestamp`, and that is\n// not negotiable — it is what stops somebody writing yesterday into the field\n// that decides who was first.\n//\n// Everything ABOVE the database wants a string. Two reasons, and the second is\n// the one that bites:\n//\n// 1. The collection DSL's `datetime` means a string; the record lint, the\n// table and the calendar all read one.\n// 2. A `Timestamp` does not survive the trip to a page. Structured clone\n// (mulmoserver's view channel) drops the CLASS and leaves\n// `{ seconds, nanoseconds }`; JSON (MulmoTerminal's preview, over HTTP,\n// and the headless run) leaves `{ type: \"firestore/timestamp/1.0\", … }`.\n// `String()` of either is `\"[object Object]\"`, so a page that sorts by\n// the field — which is exactly what a first-come app does — compares\n// every row equal and falls back to whatever order it was handed. The\n// bundled gym template shipped that sort, and its queue was ordered by\n// document id rather than by time. Nothing errored; the ranks looked\n// plausible.\n//\n// So the value is normalized ONCE, at the boundary, and the string is the only\n// thing anything above ever sees.\n//\n// THE FORM IS LOAD-BEARING, not cosmetic:\n//\n// 2026-08-15T01:45:54.605987654Z\n//\n// - UTC, `Z`, and nothing else. RFC3339 allows `+09:00`, and two strings\n// with different offsets do not sort in time order — which would give back\n// the bug this module exists to remove.\n// - NINE fractional digits, always. A `Timestamp` carries nanoseconds and\n// Firestore's own order is (seconds, nanoseconds); rounding to\n// milliseconds makes two submissions in the same millisecond compare\n// equal, and same-millisecond is precisely the burst a first-come app is\n// for. Fixed WIDTH matters as much as the precision, for a reason that is\n// easy to get backwards: trimming TRAILING ZEROS is order-preserving\n// (`.605` < `.61` either way), but MIXING widths is not — `…605Z` and\n// `…605987654Z` compare at the fourth fraction character, where `Z` (90)\n// is greater than any digit, so the millisecond value sorts AFTER a\n// nanosecond value it is earlier than or equal to. One width, always.\n// - Lexicographic order is therefore chronological order, which is what lets\n// an author's page sort with a plain string compare and be right.\n//\n// TIES. Two records can still carry the same instant. The tie-break is the\n// DOCUMENT ID, and it comes for free rather than from code here: every read of\n// a shared collection is ordered by `__name__` (core's `firestoreDocs.list`\n// and mulmoserver's own reader both do it), and `Array.prototype.sort` is\n// stable, so equal keys keep that order in every host. Do not \"improve\" either\n// read into a field ordering without replacing this sentence.\n//\n// Design: mulmoterminal `plans/fix-shared-app-server-time.md` (D2/D3).\n\n/** The canonical instant, exactly: UTC, nine fractional digits, `Z`. */\nconst CANONICAL_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}Z$/;\n\nconst NANOS_PER_SECOND = 1_000_000_000;\nconst MILLIS_PER_SECOND = 1000;\nconst NANOS_PER_MILLI = 1_000_000;\n\n/** What a Firestore `Timestamp` looks like once it is only data. Duck-typed on\n * purpose: this module is pure and imports no SDK, because it runs where the\n * class is already gone — a page's payload, a JSON body, a test. */\nexport interface ServerTimeParts {\n seconds: number;\n nanoseconds: number;\n}\n\nconst isFiniteInteger = (value: unknown): value is number => typeof value === \"number\" && Number.isInteger(value);\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null;\n\n/** Carries the two numbers, whatever else it is. A guard rather than a cast:\n * what arrives here is genuinely unknown — a page's payload, a JSON body, a\n * document another client wrote. */\nconst isTimestampLike = (value: unknown): value is ServerTimeParts => isRecord(value) && isFiniteInteger(value.seconds) && isFiniteInteger(value.nanoseconds);\n\n/** Is this the canonical form, and a real instant?\n *\n * The shape check alone is not enough, and the gap is not theoretical: with it,\n * `2026-02-30T00:00:00.000000000Z` passes, the record lint reports nothing, and\n * `Date.parse` then normalises it to March 2 — so a write/read round trip\n * silently moves the value. The round trip below is the check: a string is\n * canonical only when re-formatting what it parses to gives it back. */\nexport const isCanonicalServerTime = (value: unknown): value is string => {\n if (typeof value !== \"string\" || !CANONICAL_RE.test(value)) return false;\n // Firestore's own range starts at 0001-01-01; the four digits above already\n // bound the other end. Year zero parses in JS and would only fail later, when\n // the SDK is handed it — an exception at the write instead of a refusal here.\n if (value.startsWith(\"0000-\")) return false;\n const millis = Date.parse(`${value.slice(0, 19)}Z`);\n if (Number.isNaN(millis)) return false;\n return new Date(millis).toISOString().slice(0, 19) === value.slice(0, 19);\n};\n\n/** The canonical string for a (seconds, nanoseconds) pair, or null when the\n * pair is not a real instant.\n *\n * Built from the SECONDS through `Date` and the nanoseconds as text, never by\n * turning nanoseconds into a float — `605987654 / 1e9` cannot be represented\n * exactly, and a rounding error here is a rank in the wrong place. */\nexport function serverTimeOf(parts: ServerTimeParts): string | null {\n const { seconds, nanoseconds } = parts;\n if (!isFiniteInteger(seconds) || !isFiniteInteger(nanoseconds)) return null;\n if (nanoseconds < 0 || nanoseconds >= NANOS_PER_SECOND) return null;\n const whole = new Date(seconds * MILLIS_PER_SECOND);\n if (Number.isNaN(whole.getTime())) return null;\n const formatted = `${whole.toISOString().slice(0, 19)}.${String(nanoseconds).padStart(9, \"0\")}Z`;\n // A year outside the four-digit range formats as `+275760-…`, which is not\n // this form. Checked rather than assumed, because a caller that stored the\n // result would be storing something nothing downstream recognises.\n return isCanonicalServerTime(formatted) ? formatted : null;\n}\n\n/** A stored server time in any of the shapes it reaches us in, as the\n * canonical string — or null when the value is not one at all.\n *\n * The three shapes are the three trips a `Timestamp` can make: still an SDK\n * instance (a host reading Firestore directly), structured-cloned (a page's\n * payload), or JSON (`Timestamp.toJSON`, which tags itself). All three carry\n * `seconds` and `nanoseconds`; the tag is not required, because the clone\n * drops it. */\nexport function decodeServerTime(value: unknown): string | null {\n if (!isTimestampLike(value)) return null;\n return serverTimeOf(value);\n}\n\n/** The inverse: the parts a canonical string stands for, or null for anything\n * else.\n *\n * This is what closes the write-back hole. A record is read (decoded), edited,\n * and written back WHOLE — `putItems`' upsert replaces the document — so\n * without an encode the stamp would go back as a string. The rules freeze that\n * field by comparing `diff().affectedKeys()`, so the write is refused and the\n * record can never be updated again. Dropping the field instead does not help:\n * a removed key is an affected key too.\n *\n * Deliberately narrow. Only the EXACT canonical form is encoded, and that form\n * is only ever produced by `decodeServerTime` — a human or an agent authoring\n * a `datetime` writes the civil shape, which passes through as the string it\n * is. So the codec is closed over its own output and cannot re-type somebody\n * else's data. */\nexport function encodeServerTime(value: unknown): ServerTimeParts | null {\n if (!isCanonicalServerTime(value)) return null;\n const millis = Date.parse(`${value.slice(0, 19)}Z`);\n if (Number.isNaN(millis)) return null;\n const nanoseconds = Number(value.slice(20, 29));\n return { seconds: Math.floor(millis / MILLIS_PER_SECOND), nanoseconds };\n}\n\n/** Epoch milliseconds for a canonical instant, for the places that can only\n * hold a number (a sort key, a civil placement). LOSSY below the millisecond\n * and knowingly so: whoever needs the full order compares the strings. */\nexport function serverTimeMillis(value: unknown): number | null {\n const parts = encodeServerTime(value);\n if (parts === null) return null;\n return parts.seconds * MILLIS_PER_SECOND + Math.floor(parts.nanoseconds / NANOS_PER_MILLI);\n}\n\n/** The only part of a schema this codec reads.\n *\n * Structural rather than `CollectionSchema`, because the OTHER host has its own\n * type for the same document (mulmoserver reads these records without going\n * through this package's store) and neither side should have to adopt the\n * other's declaration to call one function. */\nexport interface DeclaredFields {\n fields: Record<string, { type: string } | undefined>;\n}\n\n/** Which fields of a record may hold an instant: the ones the SCHEMA calls\n * `datetime`, and no others.\n *\n * DECLARATION, not shape. Recognising `{ seconds, nanoseconds }` wherever it\n * appears would rewrite an ordinary value that happens to have those two keys\n * — a duration, an offset, somebody's own metadata — into a datetime string,\n * and then into a Firestore timestamp on the next whole-record write. Records\n * are allowed to carry keys the schema never mentions, so that is live user\n * data being re-typed on a guess. The same argument runs the other way on the\n * write: a `string` field whose value happens to look canonical must stay a\n * string.\n *\n * A `datetime` field is the one place where both readings mean the same\n * thing, which is what makes this scope the honest one.\n *\n * WHY NOT NARROWER STILL — scoped to the app's `stampField`, the one field the\n * rules pin. It was asked for, and it is not reachable from here: that name\n * lives in `app.json` under `public.submit.<cid>`, which core reads only for\n * `aid` (see `../server/appManifest`, \"SCOPE\") and which is the AUTHORED file\n * rather than the published document the rules actually read. Getting it would\n * mean a file read per write and crossing a boundary that module states\n * deliberately.\n *\n * And the narrower scope buys less than it looks. Within this backend a\n * `datetime` field's canonical instant has ONE storage form — a timestamp —\n * and the pair above is exact, so the value re-reads as the identical string\n * for every reader. What changes for a non-stamped field is the stored TYPE,\n * which matters only to a rule comparing it as a string; the rules that touch\n * a datetime compare it to `request.time`, which is the stamped case and needs\n * the timestamp. A field that must stay a string is declared `string`. */\nconst isDateTimeField = (schema: DeclaredFields, key: string): boolean => schema.fields[key]?.type === \"datetime\";\n\n/** Every stored instant in a `datetime` field, as its canonical string; every\n * other value untouched. One level deep, which is where a stamped field\n * lives — the rules stamp a top-level key and nothing writes a server time\n * into a table row. */\nexport function decodeRecordTimes(record: Record<string, unknown>, schema: DeclaredFields): Record<string, unknown> {\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record)) {\n const decoded = isDateTimeField(schema, key) ? decodeServerTime(value) : null;\n if (decoded !== null) changed = true;\n out[key] = decoded ?? value;\n }\n // The SAME object when nothing was stamped, so a reference comparison\n // upstream keeps meaning what it meant.\n return changed ? out : record;\n}\n\n/** The write half, and it encodes ONLY what was stored as an instant.\n *\n * `previous` is the document as Firestore holds it right now. That is the only\n * honest source of provenance here: a decoded stamp is a plain string by the\n * time it comes back, indistinguishable from one an author typed or an import\n * produced, and no amount of looking at the string can tell them apart. The\n * declaration that pins the field lives in `app.json` under\n * `public.submit.<cid>`, which this package reads only for `aid`\n * (`../server/appManifest`, \"SCOPE\") — so the stored value is what is left, and\n * it answers exactly the right question: was this field an instant before?\n *\n * What that buys, concretely: the frozen stamp goes back unchanged, so a\n * whole-record write survives the rules; and a `datetime` field holding a\n * canonical-looking string that was NEVER an instant — imported data, a value\n * somebody typed — stays the string it is. The format alone would have\n * re-typed it.\n *\n * `previous` is null for a create, where nothing was stored and nothing can\n * therefore be preserved. That is correct rather than a gap: the rules require\n * a created stamp to equal `request.time`, which no client can construct, so a\n * create through this store never carries a valid one.\n *\n * `toStored` builds whatever the backend wants for an instant — a Firestore\n * `Timestamp` — and is injected so this module stays free of the SDK: it also\n * runs in a browser, where importing that would be a bundle. */\nexport function encodeRecordTimes(\n record: Record<string, unknown>,\n previous: Record<string, unknown> | null,\n toStored: (parts: ServerTimeParts) => unknown,\n): Record<string, unknown> {\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record)) {\n const wasInstant = previous !== null && isTimestampLike(previous[key]);\n const parts = wasInstant ? encodeServerTime(value) : null;\n if (parts !== null) changed = true;\n out[key] = parts === null ? value : toStored(parts);\n }\n return changed ? out : record;\n}\n","// A collection's / feed's optional accent colour, drawn as a pale chip behind\n// its launcher glyph (#2987).\n//\n// The point is telling look-alike shortcuts apart when they share a generic\n// glyph — `podcasts`, `rss_feed`, `menu_book` are all thin monochrome outlines\n// at 16px, and a pill of them reads as one texture (#2960). Measured against\n// the alternative: tinting only the glyph leaves too little coloured area to\n// register at that size; a pale background does.\n//\n// Tailwind only detects class names that appear as complete string literals,\n// so each colour's classes are spelled out here rather than built from the\n// colour name at runtime — the same rule `enumColors.ts` follows, and for the\n// same reason.\n\n/** The colours a schema may name, ordered around the hue circle.\n *\n * Deliberately EXCLUDES the warm band (red / orange / amber): those are the\n * notification severities (`ENUM_ALERT` red, `ENUM_NUDGE` amber in\n * `enumColors.ts`) and the launcher's own session badges, so an accent can\n * never make a shortcut read as \"something needs attention\".\n *\n * Seven rather than eight: rendering all eight candidates on one identical\n * glyph put `cyan` squarely between `sky` and `teal`, so a pill could hold\n * three shortcuts in the same blue-green band. A colour that cannot be told\n * from its neighbour identifies nothing, which is the entire job here — the\n * set is sized for separation, not for count. */\nexport const ACCENT_COLORS = [\"violet\", \"indigo\", \"sky\", \"teal\", \"emerald\", \"lime\", \"fuchsia\"] as const;\n\nexport type AccentColor = (typeof ACCENT_COLORS)[number];\n\n// Background + glyph colour as one literal per entry. The 50/700 pair, chosen\n// by rendering it: at 16px the GLYPH colour does most of the identifying and the\n// 50-level wash is what separates an accented shortcut from a plain white one.\n// A heavier fill would make the accent, rather than the icon, the loudest thing\n// in a chrome row that is otherwise deliberately quiet.\nconst CHIP_CLASSES: Readonly<Record<AccentColor, string>> = {\n violet: \"bg-violet-50 text-violet-700\",\n indigo: \"bg-indigo-50 text-indigo-700\",\n sky: \"bg-sky-50 text-sky-700\",\n teal: \"bg-teal-50 text-teal-700\",\n emerald: \"bg-emerald-50 text-emerald-700\",\n lime: \"bg-lime-50 text-lime-700\",\n fuchsia: \"bg-fuchsia-50 text-fuchsia-700\",\n};\n\n/** Narrowing guard: is `value` one of the colours a schema may name? */\nexport function isAccentColor(value: unknown): value is AccentColor {\n return typeof value === \"string\" && Object.hasOwn(CHIP_CLASSES, value);\n}\n\n/** The chip classes for `color`, or `null` when there is no accent to draw —\n * absent, blank, or a name this palette does not carry.\n *\n * Unknown names fail soft rather than throwing, matching how `icon` is\n * handled: a schema is user/LLM-authored, and a typo should cost the colour,\n * not the surface. The caller falls back to its own unstyled treatment. */\nexport function accentChipClasses(color: string | undefined): string | null {\n if (!isAccentColor(color)) return null;\n return CHIP_CLASSES[color];\n}\n","// Pure, deterministic helpers for the collection calendar view: parse\n// `date`-field values, build a month grid, and bucket records onto the\n// days they cover. No `Date.now()` / `new Date()` (argless) here — every\n// function takes its inputs explicitly so the logic is unit-testable\n// without faking the clock. All internal arithmetic runs in UTC (which\n// has no DST), so fixed 86_400_000 ms steps never skip or double a day.\n//\n// ONE deliberate exception, `localCivilOf`: a server-stamped instant is a point\n// in time rather than a wall clock, so asking which day it lands on is a\n// question about the reader's zone and cannot be answered in UTC without\n// answering it wrongly for most readers. It is the only function here whose\n// result depends on the environment, and its tests pin TZ.\n\nimport { serverTimeMillis } from \"./serverTime\";\n\nconst MS_PER_DAY = 86_400_000;\nconst ISO_DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n// A two-digit field (hours / minutes / seconds) of a clock value.\nconst TWO_DIGIT_RE = /^\\d{2}$/;\n// A single clock token inside a free-form `time` string field (e.g. the\n// \"14:00-17:00\" / \"17:00-\" / \"16:30\" / \"終日\" shapes seen in user data).\nconst CLOCK_RE = /(\\d{1,2}):(\\d{2})/g;\n// Range separators we tolerate between two clock tokens: ASCII hyphen, en/em\n// dash, tilde, and the Japanese wave dashes.\nconst RANGE_SEP_RE = /[-–—~〜~]/;\n\n/** Minutes in a full day — the timeline's vertical extent. */\nexport const MINUTES_PER_DAY = 1440;\n\n/** A civil date triple. `month` is 1-12 (NOT the 0-based `Date` month). */\nexport interface Ymd {\n year: number;\n month: number;\n day: number;\n}\n\n/** One cell of the 6×7 month grid. */\nexport interface DayCell {\n ymd: Ymd;\n /** False for the leading/trailing days that belong to the adjacent\n * month (rendered greyed). */\n inMonth: boolean;\n /** Canonical `YYYY-MM-DD` key for this cell. */\n key: string;\n}\n\n/** A record placed on the calendar: the inclusive `[start, end]` span of\n * days it covers. `end === start` for a single-day record. `startMin` /\n * `endMin` are minutes-of-day for the time-allocation (day) view, resolved\n * from either a `datetime` field's clock or a separate time-string field.\n * `null` means \"no clock\" — `startMin === null && endMin === null` is an\n * all-day record; a non-null `startMin` with a null `endMin` is a\n * point-in-time record (rendered as a single line). */\nexport interface RecordSpan<T> {\n item: T;\n start: Ymd;\n end: Ymd;\n startMin: number | null;\n endMin: number | null;\n}\n\nfunction pad2(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n/** Canonical `YYYY-MM-DD` string for a civil date. */\nexport function ymdKey(ymd: Ymd): string {\n return `${String(ymd.year).padStart(4, \"0\")}-${pad2(ymd.month)}-${pad2(ymd.day)}`;\n}\n\n/** Strictly parse a `YYYY-MM-DD` string into a civil date, rejecting\n * anything that isn't a real calendar day (e.g. `2026-02-30`, `2026-13-01`).\n * Returns null for non-strings and malformed values so callers can route\n * records with no usable date into the \"no date\" tray rather than crash. */\nexport function parseIsoDate(value: unknown): Ymd | null {\n if (typeof value !== \"string\") return null;\n const match = ISO_DATE_RE.exec(value.trim());\n if (!match) return null;\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n // Round-trip through a UTC Date to reject impossible days: a value the\n // Date constructor rolls over (Feb 30 → Mar 2) won't match back.\n const probe = new Date(Date.UTC(year, month - 1, day));\n if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return null;\n return { year, month, day };\n}\n\n/** Minutes-of-day for an `HH:MM` pair, or null when out of range. */\nfunction clockToMinutes(hours: number, minutes: number): number | null {\n if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;\n return hours * 60 + minutes;\n}\n\n/** Strictly parse a `YYYY-MM-DDTHH:MM` (optional `:SS`) datetime into its\n * civil date and minutes-of-day. Returns null for anything that isn't a real\n * calendar day or a valid 24h clock. */\nexport function parseIsoDateTime(value: unknown): { ymd: Ymd; minutes: number } | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n const tIndex = trimmed.indexOf(\"T\");\n if (tIndex === -1) return null;\n const ymd = parseIsoDate(trimmed.slice(0, tIndex));\n if (!ymd) return null;\n // `HH:MM` with an optional `:SS` the browser appends for non-zero seconds.\n const parts = trimmed.slice(tIndex + 1).split(\":\");\n if (parts.length < 2 || parts.length > 3 || !parts.every((part) => TWO_DIGIT_RE.test(part))) return null;\n const minutes = clockToMinutes(Number(parts[0]), Number(parts[1]));\n if (minutes === null) return null;\n return { ymd, minutes };\n}\n\n/** A server-stamped instant as the civil time it happened at HERE.\n *\n * `parseIsoDateTime` stays strict and stays civil: it is what the grid, the\n * trigger and the spawn code parse, and teaching it to swallow an absolute\n * instant would mean placing an instant on a civil grid with no zone — the\n * thing its strictness prevents. So the conversion happens at the placement\n * instead, and it happens ONCE.\n *\n * LOCAL rather than UTC, deliberately. An instant is a point in time, and the\n * question the grid answers is \"which of MY days was that\". A Tokyo evening\n * booking sits on the correct day for the person in Tokyo and on the previous\n * one in UTC. The cost is that two people in different zones can see the same\n * record on different days — which is what an absolute instant honestly means,\n * where a civil `YYYY-MM-DDTHH:MM` means the same wall clock for everyone. */\nfunction localCivilOf(value: unknown): { ymd: Ymd; minutes: number } | null {\n const millis = serverTimeMillis(value);\n if (millis === null) return null;\n const instant = new Date(millis);\n return {\n ymd: { year: instant.getFullYear(), month: instant.getMonth() + 1, day: instant.getDate() },\n minutes: instant.getHours() * 60 + instant.getMinutes(),\n };\n}\n\n/** Civil date from a `YYYY-MM-DD`, a `YYYY-MM-DDTHH:MM`, or a server-stamped\n * instant, so the month grid buckets all three alike. */\nexport function dateOf(value: unknown): Ymd | null {\n return parseIsoDate(value) ?? parseIsoDateTime(value)?.ymd ?? localCivilOf(value)?.ymd ?? null;\n}\n\n/** Minutes-of-day from a datetime value, or null for date-only / invalid. */\nfunction timeOf(value: unknown): number | null {\n return parseIsoDateTime(value)?.minutes ?? localCivilOf(value)?.minutes ?? null;\n}\n\n/** Parse a free-form time-string field into start/end minutes-of-day.\n * Handles the common shapes in user data:\n * \"14:00-17:00\" → { start: 840, end: 1020 } (range → block)\n * \"17:00-\" → { start: 1020, end: null } (open end → single line)\n * \"16:30\" → { start: 990, end: null } (point in time → single line)\n * \"終日\" / \"\" → null (no clock → all-day)\n * Returns null when no clock token is parseable. */\nexport function parseTimeRange(value: unknown): { startMin: number | null; endMin: number | null } | null {\n if (typeof value !== \"string\") return null;\n const text = value.trim();\n if (!text) return null;\n const tokens = [...text.matchAll(CLOCK_RE)];\n if (tokens.length === 0) return null;\n const minutesOf = (match: RegExpMatchArray): number | null => clockToMinutes(Number(match[1]), Number(match[2]));\n // No separator → a single point in time (start only).\n if (!RANGE_SEP_RE.test(text)) {\n const [firstToken] = tokens;\n const startMin = firstToken ? minutesOf(firstToken) : null;\n return startMin === null ? null : { startMin, endMin: null };\n }\n // Separator present → assign each token to the side of the first separator.\n const sepIndex = text.search(RANGE_SEP_RE);\n let startMin: number | null = null;\n let endMin: number | null = null;\n for (const token of tokens) {\n if ((token.index ?? 0) < sepIndex) startMin = minutesOf(token);\n else endMin = minutesOf(token);\n }\n // A start-less range (\"-09:00\") has no anchor on the timeline → treat as\n // unparseable so the record falls back to the all-day strip.\n if (startMin === null) return null;\n return { startMin, endMin };\n}\n\nfunction ymdToUtcMs(ymd: Ymd): number {\n return Date.UTC(ymd.year, ymd.month - 1, ymd.day);\n}\n\nfunction utcMsToYmd(epochMs: number): Ymd {\n const date = new Date(epochMs);\n return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() };\n}\n\n/** Chronological comparison: negative if `left` precedes `right`, 0 if the\n * same day, positive if after. */\nexport function compareYmd(left: Ymd, right: Ymd): number {\n return ymdToUtcMs(left) - ymdToUtcMs(right);\n}\n\n/** True iff `day` falls within the inclusive span `[span.start, span.end]`. */\nexport function spanCoversDay<T>(span: RecordSpan<T>, day: Ymd): boolean {\n return compareYmd(span.start, day) <= 0 && compareYmd(day, span.end) <= 0;\n}\n\n/** Build the 6×7 (42-cell) grid for the given month, including the\n * leading/trailing days of the adjacent months so every week is full.\n * `month` is 1-12. `weekStartsOn` is 0 (Sunday) … 6 (Saturday). */\nexport function buildMonthGrid(year: number, month: number, weekStartsOn = 0): DayCell[] {\n const firstWeekday = new Date(Date.UTC(year, month - 1, 1)).getUTCDay();\n const lead = (firstWeekday - weekStartsOn + 7) % 7;\n const startMs = Date.UTC(year, month - 1, 1) - lead * MS_PER_DAY;\n const cells: DayCell[] = [];\n for (let i = 0; i < 42; i++) {\n const ymd = utcMsToYmd(startMs + i * MS_PER_DAY);\n cells.push({ ymd, inMonth: ymd.year === year && ymd.month === month, key: ymdKey(ymd) });\n }\n return cells;\n}\n\n/** Resolve a record's calendar span from its date/datetime fields. Returns\n * null when the anchor date is missing/invalid (→ the caller's \"no date\"\n * tray). An end date that is missing, invalid, or earlier than the start\n * collapses to a single-day span — never an inverted range.\n *\n * Times for the day (time-allocation) view come from, in priority order:\n * 1. the clock on a `datetime` anchor/end value, else\n * 2. `timeField` — a separate free-form time-string column (e.g. \"14:00-17:00\").\n * A record with no resolvable clock has `startMin === endMin === null`. */\nexport function recordSpan<T extends Record<string, unknown>>(item: T, anchorField: string, endField?: string, timeField?: string): RecordSpan<T> | null {\n const startRaw = item[anchorField];\n const start = dateOf(startRaw);\n if (!start) return null;\n let end = start;\n let startMin = timeOf(startRaw);\n let endMin: number | null = null;\n if (endField) {\n const endRaw = item[endField];\n const parsedEnd = dateOf(endRaw);\n if (parsedEnd && compareYmd(parsedEnd, start) >= 0) {\n end = parsedEnd;\n endMin = timeOf(endRaw);\n }\n }\n // Fall back to a separate time-string field only when the date fields\n // carried no clock (the date-only anchor + `time` column shape).\n if (timeField && startMin === null && endMin === null) {\n const range = parseTimeRange(item[timeField]);\n if (range) {\n ({ startMin, endMin } = range);\n }\n }\n return { item, start, end, startMin, endMin };\n}\n\n/** Split records into those that land on the calendar (with their spans)\n * and those with no usable anchor date (the \"no date\" tray). Spans are\n * sorted by start day so same-day stacking is stable across renders. */\nexport function bucketRecords<T extends Record<string, unknown>>(\n items: readonly T[],\n anchorField: string,\n endField?: string,\n timeField?: string,\n): { spans: RecordSpan<T>[]; noDate: T[] } {\n const spans: RecordSpan<T>[] = [];\n const noDate: T[] = [];\n for (const item of items) {\n const span = recordSpan(item, anchorField, endField, timeField);\n if (span) spans.push(span);\n else noDate.push(item);\n }\n spans.sort((left, right) => compareYmd(left.start, right.start));\n return { spans, noDate };\n}\n\n/** Geometry for one record on one day of the time-allocation view.\n * `kind`:\n * \"allDay\" — no clock anywhere → render in the bottom all-day strip.\n * \"line\" — a single point in time → a 1px marker at `startMin`.\n * \"block\" — a [startMin, endMin) span, clamped to this day's [0, 1440).\n * `bleedsBefore` / `bleedsAfter` flag a multi-day span that began on an\n * earlier day or continues onto a later one (so the view can show arrows). */\nexport interface DaySlice {\n kind: \"allDay\" | \"line\" | \"block\";\n startMin: number;\n endMin: number;\n bleedsBefore: boolean;\n bleedsAfter: boolean;\n}\n\n/** Project a record's span onto a single day for the time-allocation view, or\n * null when the span doesn't cover that day. */\nexport function daySlice<T>(span: RecordSpan<T>, day: Ymd): DaySlice | null {\n if (!spanCoversDay(span, day)) return null;\n // Destructured so the `!== null` tests narrow the clocks where they are used;\n // reading them back off `span` each time would not.\n const { startMin: spanStart, endMin: spanEnd } = span;\n if (spanStart === null && spanEnd === null) {\n return { kind: \"allDay\", startMin: 0, endMin: MINUTES_PER_DAY, bleedsBefore: false, bleedsAfter: false };\n }\n const singleDay = compareYmd(span.start, span.end) === 0;\n const isStartDay = compareYmd(day, span.start) === 0;\n const isEndDay = compareYmd(day, span.end) === 0;\n // A point in time: a start clock with no end, all on one day.\n if (singleDay && spanStart !== null && spanEnd === null) {\n return { kind: \"line\", startMin: spanStart, endMin: spanStart, bleedsBefore: false, bleedsAfter: false };\n }\n const startMin = isStartDay && spanStart !== null ? spanStart : 0;\n const endMin = isEndDay && spanEnd !== null ? spanEnd : MINUTES_PER_DAY;\n // Zero-length or inverted same-day range → degrade to a line.\n if (singleDay && endMin <= startMin) {\n return { kind: \"line\", startMin, endMin: startMin, bleedsBefore: false, bleedsAfter: false };\n }\n return { kind: \"block\", startMin, endMin, bleedsBefore: !isStartDay, bleedsAfter: !isEndDay };\n}\n\n/** Side-by-side lane assignment for overlapping timeline blocks. Each input\n * is an `[startMin, endMin)` interval; the result (parallel to the input)\n * gives each item its `lane` (column index) and the `lanes` total of its\n * overlap cluster, so a renderer can size every block to `1 / lanes` width\n * and offset it by `lane / lanes`. Non-overlapping items get `lanes === 1`. */\nexport interface LaneSpan {\n startMin: number;\n endMin: number;\n}\nexport interface LaneAssignment {\n lane: number;\n lanes: number;\n}\n\n/** One input block paired with its position in the caller's array, so lane\n * assignment can sort freely and still report back in input order. */\ninterface PositionedSpan {\n index: number;\n span: LaneSpan;\n}\n\nfunction sortByStart(blocks: readonly LaneSpan[]): PositionedSpan[] {\n return [...blocks.entries()]\n .map(([index, span]) => ({ index, span }))\n .sort((left, right) => left.span.startMin - right.span.startMin || left.span.endMin - right.span.endMin);\n}\n\ninterface ClusterState {\n /** Clusters already closed. */\n done: PositionedSpan[][];\n /** The cluster still accepting blocks. */\n current: PositionedSpan[];\n /** Latest end minute seen in `current` — the cutoff for the next block. */\n end: number;\n}\n\n/** Cut the start-ordered blocks into overlap clusters: a new cluster begins at\n * the first block that starts at or after every earlier block has ended. */\nfunction splitClusters(ordered: readonly PositionedSpan[]): PositionedSpan[][] {\n // Appends into the accumulator's arrays rather than rebuilding them. The\n // spread form read well but copied both arrays on every block, so a day\n // whose blocks all overlap cost O(n²) allocations to lay out (#2765).\n const initial: ClusterState = { done: [], current: [], end: Number.NEGATIVE_INFINITY };\n const state = ordered.reduce<ClusterState>((acc, block) => {\n if (acc.current.length > 0 && block.span.startMin >= acc.end) {\n acc.done.push(acc.current);\n return { done: acc.done, current: [block], end: block.span.endMin };\n }\n acc.current.push(block);\n return { done: acc.done, current: acc.current, end: Math.max(acc.end, block.span.endMin) };\n }, initial);\n if (state.current.length > 0) state.done.push(state.current);\n return state.done;\n}\n\n/** Greedy lane packing inside one cluster: reuse the first lane already free at\n * this block's start, else open a new one. Every member reports the cluster's\n * final lane count so a renderer can size each block to `1 / lanes`. */\nfunction packCluster(cluster: readonly PositionedSpan[]): [number, LaneAssignment][] {\n const laneEnds: number[] = [];\n const placed = cluster.map(({ index, span }) => {\n const reusable = laneEnds.findIndex((end) => end <= span.startMin);\n const lane = reusable === -1 ? laneEnds.length : reusable;\n laneEnds[lane] = span.endMin;\n return { index, lane };\n });\n return placed.map(({ index, lane }) => [index, { lane, lanes: laneEnds.length }]);\n}\n\nexport function assignLanes(blocks: readonly LaneSpan[]): LaneAssignment[] {\n const clusters = splitClusters(sortByStart(blocks));\n const assignments = new Map<number, LaneAssignment>(clusters.flatMap(packCluster));\n return blocks.map((_, index) => assignments.get(index) ?? { lane: 0, lanes: 1 });\n}\n\n/** Month label key inputs — returns the 1st of the month as a `Date` so the\n * component can feed it to `Intl.DateTimeFormat(locale, …)` for a localized\n * \"April 2026\" header without this module taking a locale dependency. */\nexport function monthAnchorDate(year: number, month: number): Date {\n return new Date(Date.UTC(year, month - 1, 1));\n}\n","/**\n * A generated record id: a v4 UUID with its hyphens stripped — 32 lowercase hex\n * characters, 122 bits.\n *\n * The ONE generator behind both blank-id create paths — the server's\n * `generateItemId()` (../server/io) delegates here, so the UI's pre-filled id\n * and the id a blank-id POST mints are the same thing rather than two\n * implementations that have to agree.\n *\n * It replaced an 8-hex-char id, whose 32 bits collided at a 1.2% rate across\n * 10k records — and a collision surfaced as `item '<id>' already exists`, which\n * named a duplicate that did not exist (#2851).\n *\n * **The hyphens have to go.** A record id in a `googleCalendar` collection is\n * sent to Google as the caller-chosen event id, and Google takes base32hex only\n * (`isClientSettableEventId`, ../../google/pushPlan) — a hyphenated UUID is\n * refused, so every UI-created record would push as \"the record id cannot be\n * used as a Google event id\". Hex is a subset of base32hex, so the stripped\n * form satisfies both that and `SAFE_RECORD_ID_PATTERN` (the id is also the\n * `<id>.json` filename stem).\n */\nexport function newItemId(): string {\n return crypto.randomUUID().replace(/-/g, \"\");\n}\n\n/** @deprecated Use {@link newItemId}. Kept as a compatibility export because\n * `@mulmoclaude/core/collection` is public API (same reason as\n * `CollectionActionWhen` in ./schema). It is the NEW generator, not the old\n * 8-hex one: that width is the bug #2851 reported, and leaving a copy of it\n * reachable under an old name would keep shipping it. */\nexport const shortHexId = newItemId;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"itemId-C9s-Jd5s.js","names":[],"sources":["../src/collection/core/fieldText.ts","../src/collection/core/schema.ts","../src/collection/core/ids.ts","../src/collection/core/collectionKey.ts","../src/collection/core/serverTime.ts","../src/collection/core/accentColor.ts","../src/collection/core/calendarGrid.ts","../src/collection/core/itemId.ts"],"sourcesContent":["// Turning a record field into text.\n//\n// `CollectionItem` is `Record<string, unknown>`, so a field holds whatever the\n// record's JSON had — including arrays and objects (real workspace data has\n// plenty: weather `hourly`, GeoJSON `geometry`, `sites` lists). Bare\n// `String(value)` on one of those yields `\"[object Object]\"`, which then gets\n// compared, matched or displayed as if it were a value. Nothing throws; a\n// predicate just silently stops matching, or the UI shows `[object Object]`.\n//\n// These two helpers are the only sanctioned way to read a field as text. The\n// rule they encode was already in `itemLabelOf`: accept primitives, let\n// everything else fall through to the caller's fallback.\n\n/** A field value that has a meaningful text form. Dates are included because\n * a JSON record can carry one once it has been revived. */\nconst isTextable = (value: unknown): value is string | number | boolean | Date =>\n typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\" || value instanceof Date;\n\n/** The field's text, or `null` when it has no meaningful one (absent, or an\n * array/object that would stringify to `\"[object Object]\"`).\n *\n * Returning `null` rather than `\"\"` keeps \"the field is empty\" distinct from\n * \"the field can't be text\" — a matcher must not treat an object-valued field\n * as an empty string and match `\"\"`. */\nexport function fieldTextOrNull(value: unknown): string | null {\n if (value === undefined || value === null) return null;\n if (!isTextable(value)) return null;\n if (value instanceof Date) {\n // `new Date(\"nonsense\")` is still `instanceof Date`, and `toISOString()`\n // throws `RangeError` on it. This helper sits on the match, sort and\n // display paths, so one unparseable date in one record would take the whole\n // render down — the loud version of the bug this module exists to prevent.\n return Number.isNaN(value.getTime()) ? null : value.toISOString();\n }\n return String(value);\n}\n\n/** The field's text, or `fallback` (default `\"\"`) when it has none. Use where\n * a string is required and an empty one is a safe stand-in — display, sort\n * keys, CSV cells. Where the distinction matters, use {@link fieldTextOrNull}. */\nexport function fieldText(value: unknown, fallback = \"\"): string {\n return fieldTextOrNull(value) ?? fallback;\n}\n","// Schema-driven collection types. A \"collection\" is a skill (under\n// .claude/skills/<slug>/) that also ships a sibling `schema.json`.\n// The host's <CollectionView> reads the schema + records and renders\n// a table/form; Claude reads SKILL.md and CRUDs the records as JSON\n// files.\n//\n// SINGLE SOURCE OF TRUTH: every type describing the schema.json contract is\n// derived (`z.infer`) from the zod definitions in `./schemaZ` — the shapes,\n// their doc comments, and the validation rules live THERE; this module only\n// re-derives the TypeScript names consumers import. The imports from\n// `./schemaZ` are type-only, so zod never reaches the browser bundle through\n// the isomorphic barrel; at runtime the dependency points the other way\n// (schemaZ imports this module's consts).\n//\n// Field specs are a DISCRIMINATED UNION on `type`: narrow with\n// `field.type === \"enum\"` (etc.) before reading a variant key like `values`,\n// `to`, `formula`, or `of`.\n\nimport { fieldText } from \"./fieldText\";\nimport type { z } from \"zod\";\nimport type {\n ActionSpecZ,\n CollectionSchemaZ,\n CustomViewZ,\n DataSourceZ,\n DynamicIconRuleZ,\n DynamicIconSourceZ,\n DynamicIconSpecZ,\n EveryFieldDrivenZ,\n EveryLiteralZ,\n EveryZ,\n FieldSpecZ,\n IngestZ,\n SpawnZ,\n StorageZ,\n SubFieldSpecZ,\n WhenZ,\n} from \"./schemaZ\";\n\n/** Minimal \"this collection is a feed\" descriptor carried on the schema.\n * Deliberately narrow — the canonical collection contract stays\n * independent of the host's feeds subsystem. The host's richer retrieval\n * spec (`IngestSpec` in `feeds/ingestTypes.ts`) is a subtype, so feed code\n * reads the extra fields by typing feed schemas with that subtype;\n * collection rendering only needs these three + the presence check. */\nexport interface CollectionIngest {\n kind: string;\n schedule: string;\n /** Optional time-of-day anchor for `schedule: \"daily\"` — the hour (0–23) to\n * refresh around (the host ticks hourly, so the run lands within that hour).\n * Ignored for non-daily schedules. Absent ⇒ elapsed-based daily (\"≥24 h since\n * the last run\"). NOTE: **UTC**, not local — compared via `getUTCHours()` for\n * an unambiguous, DST-free check (matching the rest of the scheduler), so\n * convert local times before writing (e.g. 07:00 JST → `atHour: 22`). */\n atHour?: number;\n /** Declarative retrievers (`rss`/`atom`/`http-json`) only — the host fetches\n * this URL on the schedule. Absent for `kind: \"agent\"`, where the agent owns\n * retrieval. */\n url?: string;\n /** `kind: \"agent\"` only: role id the scheduled hidden worker runs in. */\n role?: string;\n /** `kind: \"agent\"` only: skill-relative template path (under `templates/`)\n * whose prose tells the worker how to refresh the records. */\n template?: string;\n}\n\n/** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's\n * feeds engine dispatches on these; they live here (with the schema contract)\n * so the schema validator can enforce them. The host re-exports these from\n * `server/workspace/feeds/ingestTypes.ts`. */\nexport const INGEST_KINDS = [\"rss\", \"atom\", \"http-json\"] as const;\nexport type IngestKind = (typeof INGEST_KINDS)[number];\n\n/** The agent-performed ingest kind. Instead of a declarative fetch, the host\n * dispatches a hidden background chat (origin `system`) in `ingest.role`,\n * seeded with `ingest.template` + a summary of every record, on the\n * `ingest.schedule` cadence; the worker edits records via the collections io\n * layer. Kept separate from {@link INGEST_KINDS} (which the declarative\n * retriever registry keys on) so the schema validator can model `ingest` as a\n * discriminated union without the feeds engine gaining an \"agent\" retriever. */\nexport const AGENT_INGEST_KIND = \"agent\" as const;\nexport type AgentIngestKind = typeof AGENT_INGEST_KIND;\n\n/** Refresh cadences a Feed's `ingest.schedule` may declare. */\nexport const FEED_SCHEDULES = [\"hourly\", \"daily\", \"weekly\", \"on-demand\"] as const;\nexport type FeedSchedule = (typeof FEED_SCHEDULES)[number];\n\n// \"feed\" collections live in the non-skill `<workspace>/feeds/` registry\n// and carry an `ingest` block; they reuse the same storage + rendering\n// as skill-backed collections but are never loaded into the agent prompt.\nexport type CollectionSource = \"user\" | \"project\" | \"feed\";\n\n/** One field of a record — a discriminated union on `type`; see the variant\n * docs in `./schemaZ` (`FieldSpecZ`). */\nexport type CollectionFieldSpec = z.infer<typeof FieldSpecZ>;\n\n/** A `table` field's row sub-schema entry — the field union minus `table` /\n * `derived` / display-only types (see `SubFieldSpecZ`). */\nexport type CollectionSubFieldSpec = z.infer<typeof SubFieldSpecZ>;\n\nexport type CollectionFieldType = CollectionFieldSpec[\"type\"];\n\n/** The computed-boolean variant — a `where` predicate bound to a field\n * name; see `FlagFieldZ`. */\nexport type CollectionFlagField = Extract<CollectionFieldSpec, { type: \"flag\" }>;\n\n/** derived/embed/backlinks/rollup/toggle/flag are host-computed or\n * projected — never written to the record JSON, so required / value\n * checks and edit-draft slots must not apply to them. THE single source\n * for \"computed\" — lives here (zod-free at runtime) so browser code\n * (`./draft`) and the zod record compiler (`./recordZ`, which re-exports\n * it) share one set instead of drifting copies. */\nexport const COMPUTED_TYPES: ReadonlySet<CollectionFieldType> = new Set<CollectionFieldType>([\"derived\", \"embed\", \"backlinks\", \"rollup\", \"toggle\", \"flag\"]);\n\n/** Optional visibility predicate: the target (an action button or a\n * field) renders only when the open record's `field` (stringified) is\n * one of `in`. Generic and domain-free — the host evaluates it against\n * the record with no knowledge of what the field means. Absent ⇒\n * always shown. */\nexport type CollectionWhen = z.infer<typeof WhenZ>;\n\n/** @deprecated Name retained for back-compat; use {@link CollectionWhen}.\n * Both actions and fields share the same predicate shape. No in-repo\n * consumers, but the package is public API (MulmoTerminal). */\n// eslint-disable-next-line sonarjs/redundant-type-aliases -- deliberate deprecated back-compat export\nexport type CollectionActionWhen = CollectionWhen;\n\n/** A schema-declared, per-record action rendered as a button in the\n * read-only detail view. Pure UI/behaviour directive — never stored,\n * never validated against record data. All domain specifics (label,\n * role, template — or the declarative `set`) live in the schema / skill\n * folder, so the host stays generic. A discriminated union on `kind`;\n * see `ActionSpecZ`. */\nexport type CollectionAction = z.infer<typeof ActionSpecZ>;\n\n/** The kind of work an action kicks off: `\"chat\"` (visible LLM chat),\n * `\"agent\"` (hidden LLM worker), or `\"mutate\"` (declarative host write,\n * no LLM). */\nexport type CollectionActionKind = CollectionAction[\"kind\"];\n\n/** The LLM-seeded action variants (`role` + `template`). */\nexport type CollectionSeededAction = Extract<CollectionAction, { kind: \"chat\" | \"agent\" }>;\n\n/** The declarative host-write variant (`set` + optional `require`/`params`). */\nexport type CollectionMutateAction = Extract<CollectionAction, { kind: \"mutate\" }>;\n\n/** A custom (LLM-authored) HTML view for a collection. The host renders\n * `file` in a sandboxed iframe over the collection's records; the view\n * reaches its data only through a slug- and capability-scoped token (see\n * `server/api/auth/viewToken.ts`). Pure data — the host holds no\n * view-specific code; meaning lives in the HTML file + this registration.\n * See `CustomViewZ` for the per-key contracts. */\nexport type CollectionCustomView = z.infer<typeof CustomViewZ>;\n\n/** What a custom view's capability token is allowed to do against the\n * collection's data endpoint. `read` returns enriched records (getItems\n * semantics); `write` validates-and-stores rows (putItems semantics).\n * There is deliberately no `delete` — a view can never do more than the\n * agent's own `manageCollection` tool.\n *\n * TWO HOST INVARIANTS ride on this token, and both are easy to break in a\n * multi-root host:\n *\n * 1. **The token carries an OPAQUE scope, never a path.** A collection's\n * identity is `(root, slug)`, so a token minted in a multi-root host has to\n * say which root — but the token is signed, not encrypted, and is handed to\n * an LLM-authored iframe. An absolute root in the payload publishes the\n * user's home directory to that iframe. Mint an opaque project id and\n * resolve it host-side.\n * 2. **The scope goes in the TOKEN, not on `dataUrl`.** `__MC_VIEW.dataUrl` is\n * a bare base URL that views concatenate onto (`+ \"?fields=…\"`,\n * `+ \"/query\"`, `+ \"/actions/…\"`, `+ \"/image?path=…\"` — see\n * `assets/helps/custom-view.md`). A host that appends its own query\n * parameter there to carry the project breaks every one of those calls. */\nexport type CollectionViewCapability = NonNullable<CollectionCustomView[\"capabilities\"]>[number];\n\n/** How a `spawn` advances the source item's `triggerField` date to\n * produce the successor's. All arithmetic is done on the civil\n * (year, month, day) triple — never by adding milliseconds — so month\n * lengths and leap years are handled correctly. */\nexport type CollectionEvery = z.infer<typeof EveryLiteralZ>;\n\n/** Recurrence unit for a `spawn.every` advance. */\nexport type CollectionRecurUnit = CollectionEvery[\"unit\"];\n\n/** Field-driven recurrence: the advance interval is selected PER RECORD by\n * the value of an `enum` field (`fromField`), looked up in `map`. See\n * `EveryFieldDrivenZ`. */\nexport type CollectionEveryFieldDriven = z.infer<typeof EveryFieldDrivenZ>;\n\n/** The `every` of a `spawn`: either a single literal interval applied to\n * every record, or a per-record interval selected by an `enum` field. The\n * literal arm is what `advanceTriggerDate` consumes — the field-driven arm\n * is resolved down to one of its `map` values before the date math runs. */\nexport type CollectionSpawnEvery = z.infer<typeof EveryZ>;\n\n/** Narrowing guard: true when `every` is the field-driven arm. */\nexport function isFieldDrivenEvery(every: CollectionSpawnEvery): every is CollectionEveryFieldDriven {\n return \"fromField\" in every;\n}\n\n/** Host-driven recurrence. See `SpawnZ`. */\nexport type CollectionSpawn = z.infer<typeof SpawnZ>;\n\n/** One rule in a `dynamicIcon.rules` list: when the resolved source\n * record matches `where` (an AND of typed conditions, see `./where`),\n * the collection's effective launcher icon becomes `icon`. Evaluated top\n * to bottom — the first match wins. */\nexport type DynamicIconRule = z.infer<typeof DynamicIconRuleZ>;\n\n/** Where a {@link DynamicIconSpec}'s source record comes from: a (possibly\n * cross-collection) pool of records, optionally narrowed by `where` and\n * reduced to a single record by `from`. */\nexport type DynamicIconSource = z.infer<typeof DynamicIconSourceZ>;\n\n/** Declarative \"data state → icon\" mapping for a collection's launcher\n * shortcut icon (see `CollectionSchema.dynamicIcon`). When absent, the\n * launcher icon is the static `schema.icon`. */\nexport type DynamicIconSpec = z.infer<typeof DynamicIconSpecZ>;\n\n/** The `ingest` block as the schema validator accepts it — a discriminated\n * union on `kind` (declarative retrievers | agent worker). The feeds\n * subsystem's `IngestSpec` is the same union under its historical name. */\nexport type CollectionIngestSpec = z.infer<typeof IngestZ>;\n\n/** The `dataSource` block: this collection's records are the rows of an\n * external read-only data file (v1: CSV). See `DataSourceZ`. */\nexport type CollectionDataSource = z.infer<typeof DataSourceZ>;\n\n/** The `storage` block: an alternative WRITABLE record backend (v1:\n * sqlite). See `StorageZ`. */\nexport type CollectionStorage = z.infer<typeof StorageZ>;\n\n/** Every storage backend a schema can select. `file` is the implicit\n * default (`dataPath`); `csv` is implied by `dataSource`; other kinds are\n * named explicitly via `storage.type`. The server's store factory registry\n * (`server/store.ts`) is keyed by this. */\nexport type CollectionStorageKind = \"file\" | \"csv\" | \"sqlite\" | \"firestore\";\n\n/** Which storage backend serves this schema's records. Derived, not stored:\n * existing schemas carry no `storage` key and must keep resolving exactly\n * as before (`dataSource` ⇒ csv, else file). */\nexport function storageKindFor(schema: Pick<CollectionSchema, \"dataSource\" | \"storage\">): CollectionStorageKind {\n if (schema.dataSource !== undefined) return \"csv\";\n return schema.storage?.type ?? \"file\";\n}\n\n/** The whole `schema.json` contract. Key-level docs live on\n * `CollectionSchemaZ` in `./schemaZ`. */\nexport type CollectionSchema = z.infer<typeof CollectionSchemaZ>;\n\n/** True when `schema` declares an external `dataSource` — i.e. the\n * collection is READ-ONLY through every UI/tool write path (updates\n * happen by editing/replacing the data file itself). Isomorphic: both\n * the server write guards and the client's control hiding key off this\n * one predicate. */\nexport function isReadOnlySchema(schema: Pick<CollectionSchema, \"dataSource\">): boolean {\n return schema.dataSource !== undefined;\n}\n\nexport interface CollectionSummary {\n slug: string;\n title: string;\n icon: string;\n /** Optional accent colour name (see `ACCENT_COLORS`), drawn as a pale chip\n * behind the launcher glyph. Absent when the schema names none.\n *\n * Declared NARROW (no `| undefined`), like its `appId` / `readonly`\n * siblings: the remote-host handlers pass these shapes through `Jsonify`,\n * which drops `undefined` from a member union, so a widened optional stops\n * being assignable there. Producers therefore ADD the key conditionally\n * rather than assigning `undefined` to it. */\n color?: string;\n source: CollectionSource;\n /** Present (true) when the collection is backed by an external\n * `dataSource` and therefore read-only in every UI/tool write path.\n * Absent-when-writable, matching the other optional summary flags. */\n readonly?: true;\n /** Slugs of the source collection(s) a `dynamicIcon` icon was computed\n * from — present only when `schema.dynamicIcon` is set. Lets a client\n * know which collection change-channel(s) to watch for a live icon\n * update (see `useDynamicShortcutIcons`). */\n iconSources?: string[];\n /** The app a SHARED collection belongs to — present iff the schema declares\n * `storage.type: \"firestore\"`. A client needs it to subscribe to the right\n * live-change channel: a shared collection publishes on\n * `collection:app/<aid>/<cid>`, and a subscriber that keys on the name alone\n * listens to the LOCAL channel, so the refetch never arrives. Not a secret —\n * it is committed in the repository every clone reads. */\n appId?: string;\n}\n\nexport interface CollectionDetail extends CollectionSummary {\n schema: CollectionSchema;\n}\n\nexport type CollectionItem = Record<string, unknown>;\n\n/** Resolve an `embed` field's target record id: the fixed `id`, or the value\n * of the sibling `idField` on this record (empty string when neither applies\n * — the caller renders that as \"no record\"). Pure + isomorphic so the server\n * projection (`derive.ts`) and the client preview (`useCollectionRendering`)\n * resolve embeds identically. Non-`embed` fields resolve to \"no record\". */\nexport function embedTargetId(field: CollectionFieldSpec, record: CollectionItem | null): string {\n if (field.type !== \"embed\") return \"\";\n if (field.id) return field.id;\n if (field.idField && record) return fieldText(record[field.idField]);\n return \"\";\n}\n","// Pure slug / record-id character rules. Shared by the isomorphic schema\n// validator (`./schemaZ`) — which must stay node-free — and the server-side\n// path sanitisers (`../server/paths`), which wrap these patterns with the\n// `path.basename` round-trip CodeQL recognises as a `js/path-injection`\n// sanitiser. Both layers MUST gate on the same patterns; importing them from\n// here is what keeps them in sync.\n\n// The ONE slug pattern — `server/workspace/skills/catalog.ts` imports it\n// for its own sanitiser, so there is no second copy to keep in sync.\n// Bounded character classes, no nested quantifiers; ReDoS-safe.\nimport { fieldText } from \"./fieldText\";\nimport type { CollectionItem } from \"./schema\";\n\n// eslint-disable-next-line security/detect-unsafe-regex -- non-overlapping character classes, no catastrophic backtracking\nexport const SAFE_SLUG_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;\n\n// Record ids are a superset of slugs: they're only ever filename stems\n// (`<id>.json`), never directory names or URL segments, so they may carry\n// dots — natural keys like a Slack ts (`1718900000.123456`), a SemVer\n// (`1.2.3`), or a decimal timestamp. The interior class adds `.` to the slug\n// set; the explicit `..` reject in `isSafeRecordId` keeps a\n// parent-dir-looking segment out while still allowing repeated `-`/`_`\n// (`a--b`, `a__b`). Start/end stay alphanumeric so leading/trailing dots\n// (hidden files, the special `.`/`..` names) and `..`-only ids are all\n// excluded.\n// eslint-disable-next-line security/detect-unsafe-regex -- non-overlapping character classes, no catastrophic backtracking\nexport const SAFE_RECORD_ID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;\n\n/** True when `value` is a well-formed collection slug (alphanumeric /\n * hyphen / underscore, no path separators). The pattern admits no `/`,\n * `\\`, or `.`, so a passing value is trivially also a safe basename —\n * validation callers need no `path.basename` round-trip (path-building\n * callers use `../server/paths#safeSlugName`, which adds it). */\nexport function isSafeSlug(value: string): boolean {\n return typeof value === \"string\" && SAFE_SLUG_PATTERN.test(value);\n}\n\n/** True when `value` is a well-formed record id (slug charset plus interior\n * dots), with any `..` substring rejected explicitly. Validation-only\n * counterpart of `../server/paths#safeRecordId`. */\nexport function isSafeRecordId(value: string): boolean {\n if (typeof value !== \"string\" || !SAFE_RECORD_ID_PATTERN.test(value)) return false;\n return !value.includes(\"..\");\n}\n\nconst DEFAULT_UNIQUE_ID_ATTEMPTS = 8;\n\n/** Pick an id not already in `existing`, re-rolling `generate()` up to\n * `maxAttempts` times before giving up and returning the last candidate.\n * Collisions on a wide id space are astronomically unlikely, so a caller's\n * own overwrite guard is the final backstop rather than an unbounded loop. */\nexport function generateUniqueId(existing: ReadonlySet<string>, generate: () => string, maxAttempts: number = DEFAULT_UNIQUE_ID_ATTEMPTS): string {\n let candidate = generate();\n for (let attempt = 0; attempt < maxAttempts && existing.has(candidate); attempt++) {\n candidate = generate();\n }\n return candidate;\n}\n\n/** An id not already used by any loaded record: collects the in-memory primary\n * keys and re-rolls `generate()` against them (see {@link generateUniqueId}).\n * The pure core of the view's create-form id prefill. */\nexport function nextUniqueItemId(\n items: readonly CollectionItem[],\n primaryKey: string,\n generate: () => string,\n maxAttempts: number = DEFAULT_UNIQUE_ID_ATTEMPTS,\n): string {\n const existing = new Set(items.map((item) => fieldText(item[primaryKey])));\n return generateUniqueId(existing, generate, maxAttempts);\n}\n","// What a collection IS, as a value.\n//\n// Until now a collection's identity was `(root, slug)` — see the INVARIANT on\n// `CollectionHost` — and that is still exactly right for a collection that\n// lives in a directory. A SHARED collection does not: it is published to\n// Firestore under `apps/{aid}/collections/{cid}`, several machines resolve it,\n// and no one of their paths is its name. Keying it by the path it happened to\n// be published from would make the same collection two collections.\n//\n// The two identities COEXIST. A local collection's behaviour must not change at\n// all, so this is a discriminated union rather than a widening of either one:\n// every surface the INVARIANT enumerates — a cache, a pubsub channel, a view\n// token, a notification id, a rendered card — keys on this type, and the\n// compiler then refuses the thing that keeps happening by hand, which is keying\n// on the NAME alone (`slug` / `cid`) and letting two collections that share a\n// name collide.\n//\n// Isomorphic on purpose: a card and a channel name are decided on both sides of\n// the wire, so this module imports nothing from node.\n\nimport { SAFE_SLUG_PATTERN } from \"./ids\";\n\n/** A collection in a directory. `root` must already be canonical\n * (`canonicalRoot`) — it is an identity here, not a path to read, and `/proj`\n * vs `/proj/` would be two collections. Server callers should build these\n * through `localCollectionKey` in `collection/server`, which canonicalises;\n * the arm is spelled out here so the type itself stays isomorphic. */\nexport interface LocalCollectionKey {\n kind: \"local\";\n root: string;\n slug: string;\n}\n\n/** A collection published to a shared app: `apps/{aid}/collections/{cid}`.\n * `aid` is committed in the repository, so every clone resolves the same\n * collection and an invitation is about authorization, never discovery. */\nexport interface SharedCollectionKey {\n kind: \"shared\";\n aid: string;\n cid: string;\n}\n\nexport type CollectionKey = LocalCollectionKey | SharedCollectionKey;\n\nexport const isLocalCollectionKey = (key: CollectionKey): key is LocalCollectionKey => key.kind === \"local\";\nexport const isSharedCollectionKey = (key: CollectionKey): key is SharedCollectionKey => key.kind === \"shared\";\n\n/** Build a local key from an ALREADY-CANONICAL root — and refuse one that is\n * not. See {@link scopePart}. */\nexport const localCollectionKeyOf = (root: string, slug: string): LocalCollectionKey => ({\n kind: \"local\",\n root: scopePart(root, \"root\"),\n slug: namePart(slug, \"slug\"),\n});\n\n/** Build a shared key. */\nexport const sharedCollectionKey = (aid: string, cid: string): SharedCollectionKey => ({\n kind: \"shared\",\n aid: namePart(aid, \"aid\"),\n cid: namePart(cid, \"cid\"),\n});\n\n/** The collection's NAME within its scope: the slug, or the shared `cid`.\n *\n * This is what a schema file, a URL segment and a label are keyed by, and it\n * is deliberately NOT enough to identify a collection — that is the whole\n * point of the union. Use it to look things up INSIDE a known scope, never as\n * a map key across scopes. */\nexport const collectionKeyName = (key: CollectionKey): string => (key.kind === \"local\" ? key.slug : key.cid);\n\n// NUL separates the parts: it cannot occur in a path, a slug, an app id or a\n// collection id, so the encoding is unambiguous and needs no escaping.\nconst SEP = \"\\u0000\";\n\n/** A ROOT: any non-empty string that can be encoded. A path's charset is the\n * filesystem's, so the only thing to require is that it survives the encoding.\n *\n * \"NUL cannot occur in a path\" is true of every real value and is the reason\n * the encoding needs no escaping — but a type whose whole job is to be an\n * identity must not take the claim on trust. Without this, `(\"a\\0b\", \"c\")`\n * and `(\"a\", \"b\\0c\")` encode to the SAME string, so `sameCollectionKey` calls\n * two different collections equal and the id parses back to nothing. Empty is\n * refused for the same reason: it makes the id ambiguous about which part was\n * missing.\n *\n * A throw, where `parseCollectionKeyId` returns null: building a key is code\n * making an identity, and a bad one there is a programming error. Parsing is\n * reading something off a disk or a wire, where an unrecognised entry is a\n * thing to skip. */\nfunction scopePart(value: string, field: string): string {\n if (value.length === 0) throw new Error(`CollectionKey: ${field} must not be empty`);\n if (value.includes(SEP)) throw new Error(`CollectionKey: ${field} must not contain NUL`);\n if (!isCanonicalRootShape(value)) throw new Error(`CollectionKey: ${field} \"${value}\" is not a canonical root`);\n return value;\n}\n\n/** Is this root ALREADY in the shape `canonicalRoot` (path.resolve) produces?\n *\n * This module cannot canonicalise — that needs `node:path` and this file is\n * isomorphic — so it does the other half: it REFUSES anything that is not\n * already canonical. Which is the part that matters, because the failure is\n * silent. `/work/proj/` and `/work/proj` are the same collection, and a key\n * built from the first compares unequal to a key built from the second: two\n * cache entries, two channels, two bells for one collection — exactly the\n * identity split this type exists to remove. A decoder reading ids off a disk\n * is where such a spelling arrives.\n *\n * The properties are `path.resolve`'s own: absolute, no `.` or `..` segment,\n * no doubled separator, no trailing one. The Windows drive form is allowed so\n * a canonical root there is not refused. */\nfunction isCanonicalRootShape(root: string): boolean {\n const windows = /^[A-Za-z]:\\\\/.test(root);\n const sep = windows ? \"\\\\\" : \"/\";\n if (!windows && !root.startsWith(\"/\")) return false;\n if (root.length > 1 && root.endsWith(sep)) return false;\n if (root.includes(sep + sep)) return false;\n return !root.split(sep).some((segment) => segment === \".\" || segment === \"..\");\n}\n\n/** A NAME — a slug, a shared `cid`, or an `aid`: the collection-slug charset,\n * `[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?`.\n *\n * THIS TYPE IS THE SINGLE SOURCE OF TRUTH for what a name may be, and that is\n * the point rather than a nicety. A name is re-encoded by every downstream\n * identity — the completion-bell id (`<scope>\\0<name>:<itemId>`, split at the\n * first colon), a pubsub channel (`collection:app/<aid>/<name>`), a cache key\n * — and each of those has a different character it cannot survive. With the\n * rule stated only downstream, the layers disagree: a cid of `sales:2026`\n * builds fine, then the bell id decodes as a DIFFERENT collection and the\n * channel name throws inside a publisher whose catch swallows it, so the live\n * update simply stops arriving. One rule here makes every encoding safe by\n * construction; the checks downstream stay as belt-and-braces for callers that\n * never went through a key.\n *\n * A local slug is already `safeSlugName`-validated upstream, so this changes\n * nothing for it. A shared `aid` / `cid` had no gate at all — they are\n * Firestore document ids under `apps/{aid}/collections/{cid}` AND the name the\n * same collection has on disk, so the slug charset is what they already are. */\nfunction namePart(value: string, field: string): string {\n if (!isValidCollectionName(value)) throw new Error(`CollectionKey: ${field} \"${value}\" is not a valid collection name`);\n return value;\n}\n\n/** The name rule as a predicate, for the encoders that take RAW strings rather\n * than a key (the completion-bell id, a channel name). They are reachable by a\n * caller that never built a key, and each has its own delimiter to be broken\n * by, so they need to apply the same rule — not a rule of their own, which is\n * how the layers came to disagree in the first place. */\nexport function isValidCollectionName(value: string): boolean {\n return SAFE_SLUG_PATTERN.test(value);\n}\n\n/** A stable string form, for the places that need a primitive key: a Map, a\n * pubsub channel name, a notification id, a card's reconciliation key.\n *\n * Round-trips through {@link parseCollectionKeyId}. The `kind` is written\n * first so a local key and a shared key can never collide however their parts\n * are spelled. */\nexport const collectionKeyId = (key: CollectionKey): string =>\n key.kind === \"local\" ? `local${SEP}${key.root}${SEP}${key.slug}` : `shared${SEP}${key.aid}${SEP}${key.cid}`;\n\n/** Decode a {@link collectionKeyId}, or `null` when the string did not come\n * from one — or carries values no key may hold.\n *\n * Decoding goes THROUGH the constructors. Building the union here directly\n * would let a string off a disk or a wire mint an identity the constructors\n * refuse: `shared\\0salon\\0sales:2026` would become a key whose name the\n * completion-bell id and the pubsub channel cannot represent, and the single\n * source of truth for a name would have a back door.\n *\n * Null rather than a throw, and a throw from a constructor caught here: these\n * strings are read back from storage, where an unrecognised or stale entry is\n * a thing to skip, not a crash.\n *\n * A non-canonical root is refused here too, for the same reason: `/work/proj/`\n * and `/work/proj` name one collection, and two keys that compare unequal are\n * two cache entries, two channels and two bells for it. This module cannot\n * canonicalise (that needs `node:path`), so it rejects instead — see\n * {@link scopePart}. */\nexport function parseCollectionKeyId(encoded: string): CollectionKey | null {\n const parts = encoded.split(SEP);\n if (parts.length !== 3) return null;\n const [kind, first, second] = parts;\n if (kind === undefined || first === undefined || second === undefined) return null;\n try {\n if (kind === \"local\") return localCollectionKeyOf(first, second);\n if (kind === \"shared\") return sharedCollectionKey(first, second);\n } catch {\n return null;\n }\n return null;\n}\n\n/** Do two keys name the same collection? */\nexport const sameCollectionKey = (one: CollectionKey, other: CollectionKey): boolean => collectionKeyId(one) === collectionKeyId(other);\n","// The one shape a server-stamped instant takes, and the codec that keeps it\n// that way.\n//\n// A shared (firestore) collection can pin a field to the SERVER's clock:\n// `public.submit.<cid>.stampField` makes the rules require\n// `request.resource.data[field] == request.time` on create and freeze it\n// afterwards. What is stored is therefore a Firestore `Timestamp`, and that is\n// not negotiable — it is what stops somebody writing yesterday into the field\n// that decides who was first.\n//\n// Everything ABOVE the database wants a string. Two reasons, and the second is\n// the one that bites:\n//\n// 1. The collection DSL's `datetime` means a string; the record lint, the\n// table and the calendar all read one.\n// 2. A `Timestamp` does not survive the trip to a page. Structured clone\n// (mulmoserver's view channel) drops the CLASS and leaves\n// `{ seconds, nanoseconds }`; JSON (MulmoTerminal's preview, over HTTP,\n// and the headless run) leaves `{ type: \"firestore/timestamp/1.0\", … }`.\n// `String()` of either is `\"[object Object]\"`, so a page that sorts by\n// the field — which is exactly what a first-come app does — compares\n// every row equal and falls back to whatever order it was handed. The\n// bundled gym template shipped that sort, and its queue was ordered by\n// document id rather than by time. Nothing errored; the ranks looked\n// plausible.\n//\n// So the value is normalized ONCE, at the boundary, and the string is the only\n// thing anything above ever sees.\n//\n// THE FORM IS LOAD-BEARING, not cosmetic:\n//\n// 2026-08-15T01:45:54.605987654Z\n//\n// - UTC, `Z`, and nothing else. RFC3339 allows `+09:00`, and two strings\n// with different offsets do not sort in time order — which would give back\n// the bug this module exists to remove.\n// - NINE fractional digits, always. A `Timestamp` carries nanoseconds and\n// Firestore's own order is (seconds, nanoseconds); rounding to\n// milliseconds makes two submissions in the same millisecond compare\n// equal, and same-millisecond is precisely the burst a first-come app is\n// for. Fixed WIDTH matters as much as the precision, for a reason that is\n// easy to get backwards: trimming TRAILING ZEROS is order-preserving\n// (`.605` < `.61` either way), but MIXING widths is not — `…605Z` and\n// `…605987654Z` compare at the fourth fraction character, where `Z` (90)\n// is greater than any digit, so the millisecond value sorts AFTER a\n// nanosecond value it is earlier than or equal to. One width, always.\n// - Lexicographic order is therefore chronological order, which is what lets\n// an author's page sort with a plain string compare and be right.\n//\n// TIES. Two records can still carry the same instant. The tie-break is the\n// DOCUMENT ID, and it comes for free rather than from code here: every read of\n// a shared collection is ordered by `__name__` (core's `firestoreDocs.list`\n// and mulmoserver's own reader both do it), and `Array.prototype.sort` is\n// stable, so equal keys keep that order in every host. Do not \"improve\" either\n// read into a field ordering without replacing this sentence.\n//\n// Design: mulmoterminal `plans/fix-shared-app-server-time.md` (D2/D3).\n\n/** The canonical instant, exactly: UTC, nine fractional digits, `Z`. */\nconst CANONICAL_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}Z$/;\n\nconst NANOS_PER_SECOND = 1_000_000_000;\nconst MILLIS_PER_SECOND = 1000;\nconst NANOS_PER_MILLI = 1_000_000;\n\n/** What a Firestore `Timestamp` looks like once it is only data. Duck-typed on\n * purpose: this module is pure and imports no SDK, because it runs where the\n * class is already gone — a page's payload, a JSON body, a test. */\nexport interface ServerTimeParts {\n seconds: number;\n nanoseconds: number;\n}\n\nconst isFiniteInteger = (value: unknown): value is number => typeof value === \"number\" && Number.isInteger(value);\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null;\n\n/** Carries the two numbers, whatever else it is. A guard rather than a cast:\n * what arrives here is genuinely unknown — a page's payload, a JSON body, a\n * document another client wrote. */\nconst isTimestampLike = (value: unknown): value is ServerTimeParts => isRecord(value) && isFiniteInteger(value.seconds) && isFiniteInteger(value.nanoseconds);\n\n/** Is this the canonical form, and a real instant?\n *\n * The shape check alone is not enough, and the gap is not theoretical: with it,\n * `2026-02-30T00:00:00.000000000Z` passes, the record lint reports nothing, and\n * `Date.parse` then normalises it to March 2 — so a write/read round trip\n * silently moves the value. The round trip below is the check: a string is\n * canonical only when re-formatting what it parses to gives it back. */\nexport const isCanonicalServerTime = (value: unknown): value is string => {\n if (typeof value !== \"string\" || !CANONICAL_RE.test(value)) return false;\n // Firestore's own range starts at 0001-01-01; the four digits above already\n // bound the other end. Year zero parses in JS and would only fail later, when\n // the SDK is handed it — an exception at the write instead of a refusal here.\n if (value.startsWith(\"0000-\")) return false;\n const millis = Date.parse(`${value.slice(0, 19)}Z`);\n if (Number.isNaN(millis)) return false;\n return new Date(millis).toISOString().slice(0, 19) === value.slice(0, 19);\n};\n\n/** The canonical string for a (seconds, nanoseconds) pair, or null when the\n * pair is not a real instant.\n *\n * Built from the SECONDS through `Date` and the nanoseconds as text, never by\n * turning nanoseconds into a float — `605987654 / 1e9` cannot be represented\n * exactly, and a rounding error here is a rank in the wrong place. */\nexport function serverTimeOf(parts: ServerTimeParts): string | null {\n const { seconds, nanoseconds } = parts;\n if (!isFiniteInteger(seconds) || !isFiniteInteger(nanoseconds)) return null;\n if (nanoseconds < 0 || nanoseconds >= NANOS_PER_SECOND) return null;\n const whole = new Date(seconds * MILLIS_PER_SECOND);\n if (Number.isNaN(whole.getTime())) return null;\n const formatted = `${whole.toISOString().slice(0, 19)}.${String(nanoseconds).padStart(9, \"0\")}Z`;\n // A year outside the four-digit range formats as `+275760-…`, which is not\n // this form. Checked rather than assumed, because a caller that stored the\n // result would be storing something nothing downstream recognises.\n return isCanonicalServerTime(formatted) ? formatted : null;\n}\n\n/** A stored server time in any of the shapes it reaches us in, as the\n * canonical string — or null when the value is not one at all.\n *\n * The three shapes are the three trips a `Timestamp` can make: still an SDK\n * instance (a host reading Firestore directly), structured-cloned (a page's\n * payload), or JSON (`Timestamp.toJSON`, which tags itself). All three carry\n * `seconds` and `nanoseconds`; the tag is not required, because the clone\n * drops it. */\nexport function decodeServerTime(value: unknown): string | null {\n if (!isTimestampLike(value)) return null;\n return serverTimeOf(value);\n}\n\n/** The inverse: the parts a canonical string stands for, or null for anything\n * else.\n *\n * This is what closes the write-back hole. A record is read (decoded), edited,\n * and written back WHOLE — `putItems`' upsert replaces the document — so\n * without an encode the stamp would go back as a string. The rules freeze that\n * field by comparing `diff().affectedKeys()`, so the write is refused and the\n * record can never be updated again. Dropping the field instead does not help:\n * a removed key is an affected key too.\n *\n * Deliberately narrow. Only the EXACT canonical form is encoded, and that form\n * is only ever produced by `decodeServerTime` — a human or an agent authoring\n * a `datetime` writes the civil shape, which passes through as the string it\n * is. So the codec is closed over its own output and cannot re-type somebody\n * else's data. */\nexport function encodeServerTime(value: unknown): ServerTimeParts | null {\n if (!isCanonicalServerTime(value)) return null;\n const millis = Date.parse(`${value.slice(0, 19)}Z`);\n if (Number.isNaN(millis)) return null;\n const nanoseconds = Number(value.slice(20, 29));\n return { seconds: Math.floor(millis / MILLIS_PER_SECOND), nanoseconds };\n}\n\n/** Epoch milliseconds for a canonical instant, for the places that can only\n * hold a number (a sort key, a civil placement). LOSSY below the millisecond\n * and knowingly so: whoever needs the full order compares the strings. */\nexport function serverTimeMillis(value: unknown): number | null {\n const parts = encodeServerTime(value);\n if (parts === null) return null;\n return parts.seconds * MILLIS_PER_SECOND + Math.floor(parts.nanoseconds / NANOS_PER_MILLI);\n}\n\n/** The only part of a schema this codec reads.\n *\n * Structural rather than `CollectionSchema`, because the OTHER host has its own\n * type for the same document (mulmoserver reads these records without going\n * through this package's store) and neither side should have to adopt the\n * other's declaration to call one function. */\nexport interface DeclaredFields {\n fields: Record<string, { type: string } | undefined>;\n}\n\n/** Which fields of a record may hold an instant: the ones the SCHEMA calls\n * `datetime`, and no others.\n *\n * DECLARATION, not shape. Recognising `{ seconds, nanoseconds }` wherever it\n * appears would rewrite an ordinary value that happens to have those two keys\n * — a duration, an offset, somebody's own metadata — into a datetime string,\n * and then into a Firestore timestamp on the next whole-record write. Records\n * are allowed to carry keys the schema never mentions, so that is live user\n * data being re-typed on a guess. The same argument runs the other way on the\n * write: a `string` field whose value happens to look canonical must stay a\n * string.\n *\n * A `datetime` field is the one place where both readings mean the same\n * thing, which is what makes this scope the honest one.\n *\n * WHY NOT NARROWER STILL — scoped to the app's `stampField`, the one field the\n * rules pin. It was asked for, and it is not reachable from here: that name\n * lives in `app.json` under `public.submit.<cid>`, which core reads only for\n * `aid` (see `../server/appManifest`, \"SCOPE\") and which is the AUTHORED file\n * rather than the published document the rules actually read. Getting it would\n * mean a file read per write and crossing a boundary that module states\n * deliberately.\n *\n * And the narrower scope buys less than it looks. Within this backend a\n * `datetime` field's canonical instant has ONE storage form — a timestamp —\n * and the pair above is exact, so the value re-reads as the identical string\n * for every reader. What changes for a non-stamped field is the stored TYPE,\n * which matters only to a rule comparing it as a string; the rules that touch\n * a datetime compare it to `request.time`, which is the stamped case and needs\n * the timestamp. A field that must stay a string is declared `string`. */\nconst isDateTimeField = (schema: DeclaredFields, key: string): boolean => schema.fields[key]?.type === \"datetime\";\n\n/** Every stored instant in a `datetime` field, as its canonical string; every\n * other value untouched. One level deep, which is where a stamped field\n * lives — the rules stamp a top-level key and nothing writes a server time\n * into a table row. */\nexport function decodeRecordTimes(record: Record<string, unknown>, schema: DeclaredFields): Record<string, unknown> {\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record)) {\n const decoded = isDateTimeField(schema, key) ? decodeServerTime(value) : null;\n if (decoded !== null) changed = true;\n out[key] = decoded ?? value;\n }\n // The SAME object when nothing was stamped, so a reference comparison\n // upstream keeps meaning what it meant.\n return changed ? out : record;\n}\n\n/** The write half, and it encodes ONLY what was stored as an instant.\n *\n * `previous` is the document as Firestore holds it right now. That is the only\n * honest source of provenance here: a decoded stamp is a plain string by the\n * time it comes back, indistinguishable from one an author typed or an import\n * produced, and no amount of looking at the string can tell them apart. The\n * declaration that pins the field lives in `app.json` under\n * `public.submit.<cid>`, which this package reads only for `aid`\n * (`../server/appManifest`, \"SCOPE\") — so the stored value is what is left, and\n * it answers exactly the right question: was this field an instant before?\n *\n * What that buys, concretely: the frozen stamp goes back unchanged, so a\n * whole-record write survives the rules; and a `datetime` field holding a\n * canonical-looking string that was NEVER an instant — imported data, a value\n * somebody typed — stays the string it is. The format alone would have\n * re-typed it.\n *\n * `previous` is null for a create, where nothing was stored and nothing can\n * therefore be preserved. That is correct rather than a gap: the rules require\n * a created stamp to equal `request.time`, which no client can construct, so a\n * create through this store never carries a valid one.\n *\n * `toStored` builds whatever the backend wants for an instant — a Firestore\n * `Timestamp` — and is injected so this module stays free of the SDK: it also\n * runs in a browser, where importing that would be a bundle. */\nexport function encodeRecordTimes(\n record: Record<string, unknown>,\n previous: Record<string, unknown> | null,\n toStored: (parts: ServerTimeParts) => unknown,\n): Record<string, unknown> {\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record)) {\n const wasInstant = previous !== null && isTimestampLike(previous[key]);\n const parts = wasInstant ? encodeServerTime(value) : null;\n if (parts !== null) changed = true;\n out[key] = parts === null ? value : toStored(parts);\n }\n return changed ? out : record;\n}\n","// A collection's / feed's optional accent colour, drawn as a pale chip behind\n// its launcher glyph (#2987).\n//\n// The point is telling look-alike shortcuts apart when they share a generic\n// glyph — `podcasts`, `rss_feed`, `menu_book` are all thin monochrome outlines\n// at 16px, and a pill of them reads as one texture (#2960). Measured against\n// the alternative: tinting only the glyph leaves too little coloured area to\n// register at that size; a pale background does.\n//\n// Tailwind only detects class names that appear as complete string literals,\n// so each colour's classes are spelled out here rather than built from the\n// colour name at runtime — the same rule `enumColors.ts` follows, and for the\n// same reason.\n\n/** The colours a schema may name, ordered around the hue circle.\n *\n * Deliberately EXCLUDES the warm band (red / orange / amber): those are the\n * notification severities (`ENUM_ALERT` red, `ENUM_NUDGE` amber in\n * `enumColors.ts`) and the launcher's own session badges, so an accent can\n * never make a shortcut read as \"something needs attention\".\n *\n * Seven rather than eight: rendering all eight candidates on one identical\n * glyph put `cyan` squarely between `sky` and `teal`, so a pill could hold\n * three shortcuts in the same blue-green band. A colour that cannot be told\n * from its neighbour identifies nothing, which is the entire job here — the\n * set is sized for separation, not for count. */\nexport const ACCENT_COLORS = [\"violet\", \"indigo\", \"sky\", \"teal\", \"emerald\", \"lime\", \"fuchsia\"] as const;\n\nexport type AccentColor = (typeof ACCENT_COLORS)[number];\n\n// Background + glyph colour as one literal per entry. The 50/700 pair, chosen\n// by rendering it: at 16px the GLYPH colour does most of the identifying and the\n// 50-level wash is what separates an accented shortcut from a plain white one.\n// A heavier fill would make the accent, rather than the icon, the loudest thing\n// in a chrome row that is otherwise deliberately quiet.\nconst CHIP_CLASSES: Readonly<Record<AccentColor, string>> = {\n violet: \"bg-violet-50 text-violet-700\",\n indigo: \"bg-indigo-50 text-indigo-700\",\n sky: \"bg-sky-50 text-sky-700\",\n teal: \"bg-teal-50 text-teal-700\",\n emerald: \"bg-emerald-50 text-emerald-700\",\n lime: \"bg-lime-50 text-lime-700\",\n fuchsia: \"bg-fuchsia-50 text-fuchsia-700\",\n};\n\n/** Narrowing guard: is `value` one of the colours a schema may name? */\nexport function isAccentColor(value: unknown): value is AccentColor {\n return typeof value === \"string\" && Object.hasOwn(CHIP_CLASSES, value);\n}\n\n/** The chip classes for `color`, or `null` when there is no accent to draw —\n * absent, blank, or a name this palette does not carry.\n *\n * Unknown names fail soft rather than throwing, matching how `icon` is\n * handled: a schema is user/LLM-authored, and a typo should cost the colour,\n * not the surface. The caller falls back to its own unstyled treatment. */\nexport function accentChipClasses(color: string | undefined): string | null {\n if (!isAccentColor(color)) return null;\n return CHIP_CLASSES[color];\n}\n","// Pure, deterministic helpers for the collection calendar view: parse\n// `date`-field values, build a month grid, and bucket records onto the\n// days they cover. No `Date.now()` / `new Date()` (argless) here — every\n// function takes its inputs explicitly so the logic is unit-testable\n// without faking the clock. All internal arithmetic runs in UTC (which\n// has no DST), so fixed 86_400_000 ms steps never skip or double a day.\n//\n// ONE deliberate exception, `localCivilOf`: a server-stamped instant is a point\n// in time rather than a wall clock, so asking which day it lands on is a\n// question about the reader's zone and cannot be answered in UTC without\n// answering it wrongly for most readers. It is the only function here whose\n// result depends on the environment, and its tests pin TZ.\n\nimport { serverTimeMillis } from \"./serverTime\";\n\nconst MS_PER_DAY = 86_400_000;\nconst ISO_DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n// A two-digit field (hours / minutes / seconds) of a clock value.\nconst TWO_DIGIT_RE = /^\\d{2}$/;\n// A single clock token inside a free-form `time` string field (e.g. the\n// \"14:00-17:00\" / \"17:00-\" / \"16:30\" / \"終日\" shapes seen in user data).\nconst CLOCK_RE = /(\\d{1,2}):(\\d{2})/g;\n// Range separators we tolerate between two clock tokens: ASCII hyphen, en/em\n// dash, tilde, and the Japanese wave dashes.\nconst RANGE_SEP_RE = /[-–—~〜~]/;\n\n/** Minutes in a full day — the timeline's vertical extent. */\nexport const MINUTES_PER_DAY = 1440;\n\n/** A civil date triple. `month` is 1-12 (NOT the 0-based `Date` month). */\nexport interface Ymd {\n year: number;\n month: number;\n day: number;\n}\n\n/** One cell of the 6×7 month grid. */\nexport interface DayCell {\n ymd: Ymd;\n /** False for the leading/trailing days that belong to the adjacent\n * month (rendered greyed). */\n inMonth: boolean;\n /** Canonical `YYYY-MM-DD` key for this cell. */\n key: string;\n}\n\n/** A record placed on the calendar: the inclusive `[start, end]` span of\n * days it covers. `end === start` for a single-day record. `startMin` /\n * `endMin` are minutes-of-day for the time-allocation (day) view, resolved\n * from either a `datetime` field's clock or a separate time-string field.\n * `null` means \"no clock\" — `startMin === null && endMin === null` is an\n * all-day record; a non-null `startMin` with a null `endMin` is a\n * point-in-time record (rendered as a single line). */\nexport interface RecordSpan<T> {\n item: T;\n start: Ymd;\n end: Ymd;\n startMin: number | null;\n endMin: number | null;\n}\n\nfunction pad2(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n\n/** Canonical `YYYY-MM-DD` string for a civil date. */\nexport function ymdKey(ymd: Ymd): string {\n return `${String(ymd.year).padStart(4, \"0\")}-${pad2(ymd.month)}-${pad2(ymd.day)}`;\n}\n\n/** Strictly parse a `YYYY-MM-DD` string into a civil date, rejecting\n * anything that isn't a real calendar day (e.g. `2026-02-30`, `2026-13-01`).\n * Returns null for non-strings and malformed values so callers can route\n * records with no usable date into the \"no date\" tray rather than crash. */\nexport function parseIsoDate(value: unknown): Ymd | null {\n if (typeof value !== \"string\") return null;\n const match = ISO_DATE_RE.exec(value.trim());\n if (!match) return null;\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n // Round-trip through a UTC Date to reject impossible days: a value the\n // Date constructor rolls over (Feb 30 → Mar 2) won't match back.\n const probe = new Date(Date.UTC(year, month - 1, day));\n if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return null;\n return { year, month, day };\n}\n\n/** Minutes-of-day for an `HH:MM` pair, or null when out of range. */\nfunction clockToMinutes(hours: number, minutes: number): number | null {\n if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;\n return hours * 60 + minutes;\n}\n\n/** Strictly parse a `YYYY-MM-DDTHH:MM` (optional `:SS`) datetime into its\n * civil date and minutes-of-day. Returns null for anything that isn't a real\n * calendar day or a valid 24h clock. */\nexport function parseIsoDateTime(value: unknown): { ymd: Ymd; minutes: number } | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n const tIndex = trimmed.indexOf(\"T\");\n if (tIndex === -1) return null;\n const ymd = parseIsoDate(trimmed.slice(0, tIndex));\n if (!ymd) return null;\n // `HH:MM` with an optional `:SS` the browser appends for non-zero seconds.\n const parts = trimmed.slice(tIndex + 1).split(\":\");\n if (parts.length < 2 || parts.length > 3 || !parts.every((part) => TWO_DIGIT_RE.test(part))) return null;\n const minutes = clockToMinutes(Number(parts[0]), Number(parts[1]));\n if (minutes === null) return null;\n return { ymd, minutes };\n}\n\n/** A server-stamped instant as the civil time it happened at HERE.\n *\n * `parseIsoDateTime` stays strict and stays civil: it is what the grid, the\n * trigger and the spawn code parse, and teaching it to swallow an absolute\n * instant would mean placing an instant on a civil grid with no zone — the\n * thing its strictness prevents. So the conversion happens at the placement\n * instead, and it happens ONCE.\n *\n * LOCAL rather than UTC, deliberately. An instant is a point in time, and the\n * question the grid answers is \"which of MY days was that\". A Tokyo evening\n * booking sits on the correct day for the person in Tokyo and on the previous\n * one in UTC. The cost is that two people in different zones can see the same\n * record on different days — which is what an absolute instant honestly means,\n * where a civil `YYYY-MM-DDTHH:MM` means the same wall clock for everyone. */\nfunction localCivilOf(value: unknown): { ymd: Ymd; minutes: number } | null {\n const millis = serverTimeMillis(value);\n if (millis === null) return null;\n const instant = new Date(millis);\n return {\n ymd: { year: instant.getFullYear(), month: instant.getMonth() + 1, day: instant.getDate() },\n minutes: instant.getHours() * 60 + instant.getMinutes(),\n };\n}\n\n/** Civil date from a `YYYY-MM-DD`, a `YYYY-MM-DDTHH:MM`, or a server-stamped\n * instant, so the month grid buckets all three alike. */\nexport function dateOf(value: unknown): Ymd | null {\n return parseIsoDate(value) ?? parseIsoDateTime(value)?.ymd ?? localCivilOf(value)?.ymd ?? null;\n}\n\n/** Minutes-of-day from a datetime value, or null for date-only / invalid. */\nfunction timeOf(value: unknown): number | null {\n return parseIsoDateTime(value)?.minutes ?? localCivilOf(value)?.minutes ?? null;\n}\n\n/** Parse a free-form time-string field into start/end minutes-of-day.\n * Handles the common shapes in user data:\n * \"14:00-17:00\" → { start: 840, end: 1020 } (range → block)\n * \"17:00-\" → { start: 1020, end: null } (open end → single line)\n * \"16:30\" → { start: 990, end: null } (point in time → single line)\n * \"終日\" / \"\" → null (no clock → all-day)\n * Returns null when no clock token is parseable. */\nexport function parseTimeRange(value: unknown): { startMin: number | null; endMin: number | null } | null {\n if (typeof value !== \"string\") return null;\n const text = value.trim();\n if (!text) return null;\n const tokens = [...text.matchAll(CLOCK_RE)];\n if (tokens.length === 0) return null;\n const minutesOf = (match: RegExpMatchArray): number | null => clockToMinutes(Number(match[1]), Number(match[2]));\n // No separator → a single point in time (start only).\n if (!RANGE_SEP_RE.test(text)) {\n const [firstToken] = tokens;\n const startMin = firstToken ? minutesOf(firstToken) : null;\n return startMin === null ? null : { startMin, endMin: null };\n }\n // Separator present → assign each token to the side of the first separator.\n const sepIndex = text.search(RANGE_SEP_RE);\n let startMin: number | null = null;\n let endMin: number | null = null;\n for (const token of tokens) {\n if ((token.index ?? 0) < sepIndex) startMin = minutesOf(token);\n else endMin = minutesOf(token);\n }\n // A start-less range (\"-09:00\") has no anchor on the timeline → treat as\n // unparseable so the record falls back to the all-day strip.\n if (startMin === null) return null;\n return { startMin, endMin };\n}\n\nfunction ymdToUtcMs(ymd: Ymd): number {\n return Date.UTC(ymd.year, ymd.month - 1, ymd.day);\n}\n\nfunction utcMsToYmd(epochMs: number): Ymd {\n const date = new Date(epochMs);\n return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() };\n}\n\n/** Chronological comparison: negative if `left` precedes `right`, 0 if the\n * same day, positive if after. */\nexport function compareYmd(left: Ymd, right: Ymd): number {\n return ymdToUtcMs(left) - ymdToUtcMs(right);\n}\n\n/** True iff `day` falls within the inclusive span `[span.start, span.end]`. */\nexport function spanCoversDay<T>(span: RecordSpan<T>, day: Ymd): boolean {\n return compareYmd(span.start, day) <= 0 && compareYmd(day, span.end) <= 0;\n}\n\n/** Build the 6×7 (42-cell) grid for the given month, including the\n * leading/trailing days of the adjacent months so every week is full.\n * `month` is 1-12. `weekStartsOn` is 0 (Sunday) … 6 (Saturday). */\nexport function buildMonthGrid(year: number, month: number, weekStartsOn = 0): DayCell[] {\n const firstWeekday = new Date(Date.UTC(year, month - 1, 1)).getUTCDay();\n const lead = (firstWeekday - weekStartsOn + 7) % 7;\n const startMs = Date.UTC(year, month - 1, 1) - lead * MS_PER_DAY;\n const cells: DayCell[] = [];\n for (let i = 0; i < 42; i++) {\n const ymd = utcMsToYmd(startMs + i * MS_PER_DAY);\n cells.push({ ymd, inMonth: ymd.year === year && ymd.month === month, key: ymdKey(ymd) });\n }\n return cells;\n}\n\n/** Resolve a record's calendar span from its date/datetime fields. Returns\n * null when the anchor date is missing/invalid (→ the caller's \"no date\"\n * tray). An end date that is missing, invalid, or earlier than the start\n * collapses to a single-day span — never an inverted range.\n *\n * Times for the day (time-allocation) view come from, in priority order:\n * 1. the clock on a `datetime` anchor/end value, else\n * 2. `timeField` — a separate free-form time-string column (e.g. \"14:00-17:00\").\n * A record with no resolvable clock has `startMin === endMin === null`. */\nexport function recordSpan<T extends Record<string, unknown>>(\n item: T,\n anchorField: string,\n endField?: string,\n timeField?: string,\n options: SpanOptions = {},\n): RecordSpan<T> | null {\n const dated = dateSpan(item, anchorField, endField);\n if (!dated) return null;\n // The day boundary is judged from the end FIELD, before a separate time\n // column can supply a clock the end field never had.\n const bounded = options.endExclusive && endField !== undefined ? withExclusiveEnd(dated, item[endField]) : dated;\n return withTimeFieldFallback(bounded, timeField ? item[timeField] : undefined);\n}\n\n/** How a collection reads its end field. */\nexport interface SpanOptions {\n /** The end names the first moment AFTER the record, as Google Calendar's\n * `end` does: an all-day event on the 17th ends on the 18th. */\n endExclusive?: boolean;\n}\n\n/** The span options a collection's calendar uses for `endField`. Only a\n * Google Calendar mirror reads its end as exclusive — the field its sync maps\n * to `end` — because a plain collection may mean its end date inclusively. */\nexport function spanOptionsFor(schema: { googleCalendar?: { map: Record<string, string> } | undefined }, endField: string | undefined): SpanOptions {\n const mappedTo = endField === undefined ? undefined : schema.googleCalendar?.map[endField];\n return { endExclusive: mappedTo === \"end\" };\n}\n\n/** `…T00:00` or `…T00:00:00` — midnight to the second. `timeOf` keeps minutes\n * only, so `…T00:00:30` would otherwise read as midnight too. */\nconst MIDNIGHT_CLOCK_RE = /T00:00(?::00)?$/;\n\n/** Whether an end value names the very start of its day: a bare date, or a\n * datetime at exactly `00:00`. */\nexport function endsAtDayBoundary(endRaw: unknown): boolean {\n if (parseIsoDate(endRaw) !== null) return true;\n return typeof endRaw === \"string\" && parseIsoDateTime(endRaw) !== null && MIDNIGHT_CLOCK_RE.test(endRaw.trim());\n}\n\n/** An exclusive end that falls at a day boundary on a later day does not\n * occupy that day, so the span stops on the day before. An end with any other\n * clock does occupy its day and is left alone. `endRaw` is the end field's\n * own value — the span may already carry a clock from elsewhere. */\nexport function withExclusiveEnd<T>(span: RecordSpan<T>, endRaw: unknown): RecordSpan<T> {\n if (!endsAtDayBoundary(endRaw) || compareYmd(span.end, span.start) <= 0) return span;\n const end = utcMsToYmd(ymdToUtcMs(span.end) - MS_PER_DAY);\n // A clocked span runs to the end of its new last day; a clock-less one stays\n // all day. The start decides too: a `datetime` start with a bare `date` end.\n const clocked = span.endMin === 0 || span.startMin !== null;\n return { ...span, end, endMin: clocked ? MINUTES_PER_DAY : null };\n}\n\n/** The span the date fields alone describe: an end that is missing, invalid,\n * or earlier than the start collapses to the start day. */\nfunction dateSpan<T extends Record<string, unknown>>(item: T, anchorField: string, endField?: string): RecordSpan<T> | null {\n const startRaw = item[anchorField];\n const start = dateOf(startRaw);\n if (!start) return null;\n const endRaw = endField ? item[endField] : undefined;\n const parsedEnd = dateOf(endRaw);\n const usableEnd = parsedEnd !== null && compareYmd(parsedEnd, start) >= 0 ? parsedEnd : null;\n return { item, start, end: usableEnd ?? start, startMin: timeOf(startRaw), endMin: usableEnd ? timeOf(endRaw) : null };\n}\n\n/** Fall back to a separate time-string field only when the date fields\n * carried no clock (the date-only anchor + `time` column shape). */\nfunction withTimeFieldFallback<T>(span: RecordSpan<T>, timeValue: unknown): RecordSpan<T> {\n if (span.startMin !== null || span.endMin !== null) return span;\n const range = parseTimeRange(timeValue);\n return range ? { ...span, startMin: range.startMin, endMin: range.endMin } : span;\n}\n\n/** Split records into those that land on the calendar (with their spans)\n * and those with no usable anchor date (the \"no date\" tray). Spans are\n * sorted by start day so same-day stacking is stable across renders. */\nexport function bucketRecords<T extends Record<string, unknown>>(\n items: readonly T[],\n anchorField: string,\n endField?: string,\n timeField?: string,\n options: SpanOptions = {},\n): { spans: RecordSpan<T>[]; noDate: T[] } {\n const spans: RecordSpan<T>[] = [];\n const noDate: T[] = [];\n for (const item of items) {\n const span = recordSpan(item, anchorField, endField, timeField, options);\n if (span) spans.push(span);\n else noDate.push(item);\n }\n spans.sort((left, right) => compareYmd(left.start, right.start));\n return { spans, noDate };\n}\n\n/** Geometry for one record on one day of the time-allocation view.\n * `kind`:\n * \"allDay\" — no clock anywhere → render in the bottom all-day strip.\n * \"line\" — a single point in time → a 1px marker at `startMin`.\n * \"block\" — a [startMin, endMin) span, clamped to this day's [0, 1440).\n * `bleedsBefore` / `bleedsAfter` flag a multi-day span that began on an\n * earlier day or continues onto a later one (so the view can show arrows). */\nexport interface DaySlice {\n kind: \"allDay\" | \"line\" | \"block\";\n startMin: number;\n endMin: number;\n bleedsBefore: boolean;\n bleedsAfter: boolean;\n}\n\n/** Project a record's span onto a single day for the time-allocation view, or\n * null when the span doesn't cover that day. */\nexport function daySlice<T>(span: RecordSpan<T>, day: Ymd): DaySlice | null {\n if (!spanCoversDay(span, day)) return null;\n // Destructured so the `!== null` tests narrow the clocks where they are used;\n // reading them back off `span` each time would not.\n const { startMin: spanStart, endMin: spanEnd } = span;\n if (spanStart === null && spanEnd === null) {\n return { kind: \"allDay\", startMin: 0, endMin: MINUTES_PER_DAY, bleedsBefore: false, bleedsAfter: false };\n }\n const singleDay = compareYmd(span.start, span.end) === 0;\n const isStartDay = compareYmd(day, span.start) === 0;\n const isEndDay = compareYmd(day, span.end) === 0;\n // A point in time: a start clock with no end, all on one day.\n if (singleDay && spanStart !== null && spanEnd === null) {\n return { kind: \"line\", startMin: spanStart, endMin: spanStart, bleedsBefore: false, bleedsAfter: false };\n }\n const startMin = isStartDay && spanStart !== null ? spanStart : 0;\n const endMin = isEndDay && spanEnd !== null ? spanEnd : MINUTES_PER_DAY;\n // Zero-length or inverted same-day range → degrade to a line.\n if (singleDay && endMin <= startMin) {\n return { kind: \"line\", startMin, endMin: startMin, bleedsBefore: false, bleedsAfter: false };\n }\n return { kind: \"block\", startMin, endMin, bleedsBefore: !isStartDay, bleedsAfter: !isEndDay };\n}\n\n/** Side-by-side lane assignment for overlapping timeline blocks. Each input\n * is an `[startMin, endMin)` interval; the result (parallel to the input)\n * gives each item its `lane` (column index) and the `lanes` total of its\n * overlap cluster, so a renderer can size every block to `1 / lanes` width\n * and offset it by `lane / lanes`. Non-overlapping items get `lanes === 1`. */\nexport interface LaneSpan {\n startMin: number;\n endMin: number;\n}\nexport interface LaneAssignment {\n lane: number;\n lanes: number;\n}\n\n/** One input block paired with its position in the caller's array, so lane\n * assignment can sort freely and still report back in input order. */\ninterface PositionedSpan {\n index: number;\n span: LaneSpan;\n}\n\nfunction sortByStart(blocks: readonly LaneSpan[]): PositionedSpan[] {\n return [...blocks.entries()]\n .map(([index, span]) => ({ index, span }))\n .sort((left, right) => left.span.startMin - right.span.startMin || left.span.endMin - right.span.endMin);\n}\n\ninterface ClusterState {\n /** Clusters already closed. */\n done: PositionedSpan[][];\n /** The cluster still accepting blocks. */\n current: PositionedSpan[];\n /** Latest end minute seen in `current` — the cutoff for the next block. */\n end: number;\n}\n\n/** Cut the start-ordered blocks into overlap clusters: a new cluster begins at\n * the first block that starts at or after every earlier block has ended. */\nfunction splitClusters(ordered: readonly PositionedSpan[]): PositionedSpan[][] {\n // Appends into the accumulator's arrays rather than rebuilding them. The\n // spread form read well but copied both arrays on every block, so a day\n // whose blocks all overlap cost O(n²) allocations to lay out (#2765).\n const initial: ClusterState = { done: [], current: [], end: Number.NEGATIVE_INFINITY };\n const state = ordered.reduce<ClusterState>((acc, block) => {\n if (acc.current.length > 0 && block.span.startMin >= acc.end) {\n acc.done.push(acc.current);\n return { done: acc.done, current: [block], end: block.span.endMin };\n }\n acc.current.push(block);\n return { done: acc.done, current: acc.current, end: Math.max(acc.end, block.span.endMin) };\n }, initial);\n if (state.current.length > 0) state.done.push(state.current);\n return state.done;\n}\n\n/** Greedy lane packing inside one cluster: reuse the first lane already free at\n * this block's start, else open a new one. Every member reports the cluster's\n * final lane count so a renderer can size each block to `1 / lanes`. */\nfunction packCluster(cluster: readonly PositionedSpan[]): [number, LaneAssignment][] {\n const laneEnds: number[] = [];\n const placed = cluster.map(({ index, span }) => {\n const reusable = laneEnds.findIndex((end) => end <= span.startMin);\n const lane = reusable === -1 ? laneEnds.length : reusable;\n laneEnds[lane] = span.endMin;\n return { index, lane };\n });\n return placed.map(({ index, lane }) => [index, { lane, lanes: laneEnds.length }]);\n}\n\nexport function assignLanes(blocks: readonly LaneSpan[]): LaneAssignment[] {\n const clusters = splitClusters(sortByStart(blocks));\n const assignments = new Map<number, LaneAssignment>(clusters.flatMap(packCluster));\n return blocks.map((_, index) => assignments.get(index) ?? { lane: 0, lanes: 1 });\n}\n\n/** Month label key inputs — returns the 1st of the month as a `Date` so the\n * component can feed it to `Intl.DateTimeFormat(locale, …)` for a localized\n * \"April 2026\" header without this module taking a locale dependency. */\nexport function monthAnchorDate(year: number, month: number): Date {\n return new Date(Date.UTC(year, month - 1, 1));\n}\n","/**\n * A generated record id: a v4 UUID with its hyphens stripped — 32 lowercase hex\n * characters, 122 bits.\n *\n * The ONE generator behind both blank-id create paths — the server's\n * `generateItemId()` (../server/io) delegates here, so the UI's pre-filled id\n * and the id a blank-id POST mints are the same thing rather than two\n * implementations that have to agree.\n *\n * It replaced an 8-hex-char id, whose 32 bits collided at a 1.2% rate across\n * 10k records — and a collision surfaced as `item '<id>' already exists`, which\n * named a duplicate that did not exist (#2851).\n *\n * **The hyphens have to go.** A record id in a `googleCalendar` collection is\n * sent to Google as the caller-chosen event id, and Google takes base32hex only\n * (`isClientSettableEventId`, ../../google/pushPlan) — a hyphenated UUID is\n * refused, so every UI-created record would push as \"the record id cannot be\n * used as a Google event id\". Hex is a subset of base32hex, so the stripped\n * form satisfies both that and `SAFE_RECORD_ID_PATTERN` (the id is also the\n * `<id>.json` filename stem).\n */\nexport function newItemId(): string {\n return crypto.randomUUID().replace(/-/g, \"\");\n}\n\n/** @deprecated Use {@link newItemId}. Kept as a compatibility export because\n * `@mulmoclaude/core/collection` is public API (same reason as\n * `CollectionActionWhen` in ./schema). It is the NEW generator, not the old\n * 8-hex one: that width is the bug #2851 reported, and leaving a copy of it\n * reachable under an old name would keep shipping it. */\nexport const shortHexId = newItemId;\n"],"mappings":"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"}
|
|
@@ -651,41 +651,76 @@ function buildMonthGrid(year, month, weekStartsOn = 0) {
|
|
|
651
651
|
* 1. the clock on a `datetime` anchor/end value, else
|
|
652
652
|
* 2. `timeField` — a separate free-form time-string column (e.g. "14:00-17:00").
|
|
653
653
|
* A record with no resolvable clock has `startMin === endMin === null`. */
|
|
654
|
-
function recordSpan(item, anchorField, endField, timeField) {
|
|
654
|
+
function recordSpan(item, anchorField, endField, timeField, options = {}) {
|
|
655
|
+
const dated = dateSpan(item, anchorField, endField);
|
|
656
|
+
if (!dated) return null;
|
|
657
|
+
return withTimeFieldFallback(options.endExclusive && endField !== void 0 ? withExclusiveEnd(dated, item[endField]) : dated, timeField ? item[timeField] : void 0);
|
|
658
|
+
}
|
|
659
|
+
/** The span options a collection's calendar uses for `endField`. Only a
|
|
660
|
+
* Google Calendar mirror reads its end as exclusive — the field its sync maps
|
|
661
|
+
* to `end` — because a plain collection may mean its end date inclusively. */
|
|
662
|
+
function spanOptionsFor(schema, endField) {
|
|
663
|
+
return { endExclusive: (endField === void 0 ? void 0 : schema.googleCalendar?.map[endField]) === "end" };
|
|
664
|
+
}
|
|
665
|
+
/** `…T00:00` or `…T00:00:00` — midnight to the second. `timeOf` keeps minutes
|
|
666
|
+
* only, so `…T00:00:30` would otherwise read as midnight too. */
|
|
667
|
+
var MIDNIGHT_CLOCK_RE = /T00:00(?::00)?$/;
|
|
668
|
+
/** Whether an end value names the very start of its day: a bare date, or a
|
|
669
|
+
* datetime at exactly `00:00`. */
|
|
670
|
+
function endsAtDayBoundary(endRaw) {
|
|
671
|
+
if (parseIsoDate(endRaw) !== null) return true;
|
|
672
|
+
return typeof endRaw === "string" && parseIsoDateTime(endRaw) !== null && MIDNIGHT_CLOCK_RE.test(endRaw.trim());
|
|
673
|
+
}
|
|
674
|
+
/** An exclusive end that falls at a day boundary on a later day does not
|
|
675
|
+
* occupy that day, so the span stops on the day before. An end with any other
|
|
676
|
+
* clock does occupy its day and is left alone. `endRaw` is the end field's
|
|
677
|
+
* own value — the span may already carry a clock from elsewhere. */
|
|
678
|
+
function withExclusiveEnd(span, endRaw) {
|
|
679
|
+
if (!endsAtDayBoundary(endRaw) || compareYmd(span.end, span.start) <= 0) return span;
|
|
680
|
+
const end = utcMsToYmd(ymdToUtcMs(span.end) - MS_PER_DAY);
|
|
681
|
+
const clocked = span.endMin === 0 || span.startMin !== null;
|
|
682
|
+
return {
|
|
683
|
+
...span,
|
|
684
|
+
end,
|
|
685
|
+
endMin: clocked ? MINUTES_PER_DAY : null
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/** The span the date fields alone describe: an end that is missing, invalid,
|
|
689
|
+
* or earlier than the start collapses to the start day. */
|
|
690
|
+
function dateSpan(item, anchorField, endField) {
|
|
655
691
|
const startRaw = item[anchorField];
|
|
656
692
|
const start = dateOf(startRaw);
|
|
657
693
|
if (!start) return null;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (endField) {
|
|
662
|
-
const endRaw = item[endField];
|
|
663
|
-
const parsedEnd = dateOf(endRaw);
|
|
664
|
-
if (parsedEnd && compareYmd(parsedEnd, start) >= 0) {
|
|
665
|
-
end = parsedEnd;
|
|
666
|
-
endMin = timeOf(endRaw);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (timeField && startMin === null && endMin === null) {
|
|
670
|
-
const range = parseTimeRange(item[timeField]);
|
|
671
|
-
if (range) ({startMin, endMin} = range);
|
|
672
|
-
}
|
|
694
|
+
const endRaw = endField ? item[endField] : void 0;
|
|
695
|
+
const parsedEnd = dateOf(endRaw);
|
|
696
|
+
const usableEnd = parsedEnd !== null && compareYmd(parsedEnd, start) >= 0 ? parsedEnd : null;
|
|
673
697
|
return {
|
|
674
698
|
item,
|
|
675
699
|
start,
|
|
676
|
-
end,
|
|
677
|
-
startMin,
|
|
678
|
-
endMin
|
|
700
|
+
end: usableEnd ?? start,
|
|
701
|
+
startMin: timeOf(startRaw),
|
|
702
|
+
endMin: usableEnd ? timeOf(endRaw) : null
|
|
679
703
|
};
|
|
680
704
|
}
|
|
705
|
+
/** Fall back to a separate time-string field only when the date fields
|
|
706
|
+
* carried no clock (the date-only anchor + `time` column shape). */
|
|
707
|
+
function withTimeFieldFallback(span, timeValue) {
|
|
708
|
+
if (span.startMin !== null || span.endMin !== null) return span;
|
|
709
|
+
const range = parseTimeRange(timeValue);
|
|
710
|
+
return range ? {
|
|
711
|
+
...span,
|
|
712
|
+
startMin: range.startMin,
|
|
713
|
+
endMin: range.endMin
|
|
714
|
+
} : span;
|
|
715
|
+
}
|
|
681
716
|
/** Split records into those that land on the calendar (with their spans)
|
|
682
717
|
* and those with no usable anchor date (the "no date" tray). Spans are
|
|
683
718
|
* sorted by start day so same-day stacking is stable across renders. */
|
|
684
|
-
function bucketRecords(items, anchorField, endField, timeField) {
|
|
719
|
+
function bucketRecords(items, anchorField, endField, timeField, options = {}) {
|
|
685
720
|
const spans = [];
|
|
686
721
|
const noDate = [];
|
|
687
722
|
for (const item of items) {
|
|
688
|
-
const span = recordSpan(item, anchorField, endField, timeField);
|
|
723
|
+
const span = recordSpan(item, anchorField, endField, timeField, options);
|
|
689
724
|
if (span) spans.push(span);
|
|
690
725
|
else noDate.push(item);
|
|
691
726
|
}
|
|
@@ -964,6 +999,12 @@ Object.defineProperty(exports, "encodeServerTime", {
|
|
|
964
999
|
return encodeServerTime;
|
|
965
1000
|
}
|
|
966
1001
|
});
|
|
1002
|
+
Object.defineProperty(exports, "endsAtDayBoundary", {
|
|
1003
|
+
enumerable: true,
|
|
1004
|
+
get: function() {
|
|
1005
|
+
return endsAtDayBoundary;
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
967
1008
|
Object.defineProperty(exports, "fieldText", {
|
|
968
1009
|
enumerable: true,
|
|
969
1010
|
get: function() {
|
|
@@ -1126,12 +1167,24 @@ Object.defineProperty(exports, "spanCoversDay", {
|
|
|
1126
1167
|
return spanCoversDay;
|
|
1127
1168
|
}
|
|
1128
1169
|
});
|
|
1170
|
+
Object.defineProperty(exports, "spanOptionsFor", {
|
|
1171
|
+
enumerable: true,
|
|
1172
|
+
get: function() {
|
|
1173
|
+
return spanOptionsFor;
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1129
1176
|
Object.defineProperty(exports, "storageKindFor", {
|
|
1130
1177
|
enumerable: true,
|
|
1131
1178
|
get: function() {
|
|
1132
1179
|
return storageKindFor;
|
|
1133
1180
|
}
|
|
1134
1181
|
});
|
|
1182
|
+
Object.defineProperty(exports, "withExclusiveEnd", {
|
|
1183
|
+
enumerable: true,
|
|
1184
|
+
get: function() {
|
|
1185
|
+
return withExclusiveEnd;
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1135
1188
|
Object.defineProperty(exports, "ymdKey", {
|
|
1136
1189
|
enumerable: true,
|
|
1137
1190
|
get: function() {
|
|
@@ -1139,4 +1192,4 @@ Object.defineProperty(exports, "ymdKey", {
|
|
|
1139
1192
|
}
|
|
1140
1193
|
});
|
|
1141
1194
|
|
|
1142
|
-
//# sourceMappingURL=itemId-
|
|
1195
|
+
//# sourceMappingURL=itemId-CVrNibwT.cjs.map
|