@mulmoclaude/core 0.14.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/assets/helps/collection-skills.md +94 -11
  2. package/assets/helps/custom-view.md +28 -0
  3. package/assets/helps/egress-sync.md +158 -0
  4. package/assets/helps/index.md +1 -0
  5. package/dist/{calendarGrid-kO6rGfm0.js → calendarGrid-B3dtFN-m.js} +105 -2
  6. package/dist/calendarGrid-B3dtFN-m.js.map +1 -0
  7. package/dist/{calendarGrid-C1rfCXJ3.cjs → calendarGrid-CRaMAguM.cjs} +158 -1
  8. package/dist/calendarGrid-CRaMAguM.cjs.map +1 -0
  9. package/dist/collection/core/actionVisible.d.ts +15 -4
  10. package/dist/collection/core/backlinks.d.ts +40 -0
  11. package/dist/collection/core/mutateAction.d.ts +14 -0
  12. package/dist/collection/core/recordZ.d.ts +4 -5
  13. package/dist/collection/core/schema.d.ts +21 -4
  14. package/dist/collection/core/schemaZ.d.ts +209 -16
  15. package/dist/collection/core/uiTypes.d.ts +26 -0
  16. package/dist/collection/index.cjs +21 -36
  17. package/dist/collection/index.cjs.map +1 -1
  18. package/dist/collection/index.d.ts +1 -0
  19. package/dist/collection/index.js +12 -33
  20. package/dist/collection/index.js.map +1 -1
  21. package/dist/collection/registry/server/index.cjs +1 -1
  22. package/dist/collection/registry/server/index.js +1 -1
  23. package/dist/collection/server/index.cjs +8 -2
  24. package/dist/collection/server/index.d.ts +2 -0
  25. package/dist/collection/server/index.js +3 -2
  26. package/dist/collection/server/io.d.ts +9 -0
  27. package/dist/collection/server/mutate.d.ts +24 -0
  28. package/dist/collection/server/ontology.d.ts +35 -0
  29. package/dist/collection-watchers/index.cjs +5 -4
  30. package/dist/collection-watchers/index.cjs.map +1 -1
  31. package/dist/collection-watchers/index.js +3 -2
  32. package/dist/collection-watchers/index.js.map +1 -1
  33. package/dist/feeds/index.cjs +2 -2
  34. package/dist/feeds/index.js +2 -2
  35. package/dist/feeds/server/index.cjs +3 -3
  36. package/dist/feeds/server/index.js +3 -3
  37. package/dist/{ingestTypes-BIFXlw7M.cjs → ingestTypes-Cq65Bv-N.cjs} +2 -2
  38. package/dist/{ingestTypes-BIFXlw7M.cjs.map → ingestTypes-Cq65Bv-N.cjs.map} +1 -1
  39. package/dist/{ingestTypes-KuYPX9Ea.js → ingestTypes-DvsJhqMx.js} +2 -2
  40. package/dist/{ingestTypes-KuYPX9Ea.js.map → ingestTypes-DvsJhqMx.js.map} +1 -1
  41. package/dist/{server-Cc6XcyOA.js → server-0IVR-Y_G.js} +316 -35
  42. package/dist/server-0IVR-Y_G.js.map +1 -0
  43. package/dist/{server-Cd5WdM8a.cjs → server-BeoOWQ-a.cjs} +347 -42
  44. package/dist/server-BeoOWQ-a.cjs.map +1 -0
  45. package/package.json +1 -1
  46. package/dist/calendarGrid-C1rfCXJ3.cjs.map +0 -1
  47. package/dist/calendarGrid-kO6rGfm0.js.map +0 -1
  48. package/dist/server-Cc6XcyOA.js.map +0 -1
  49. package/dist/server-Cd5WdM8a.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/collection/core/presentCollection.ts","../../src/collection/core/enumColors.ts","../../src/collection/core/actionVisible.ts","../../src/collection/core/draft.ts","../../src/collection/core/sortItems.ts","../../src/collection/core/itemLabel.ts","../../src/collection/core/shortHexId.ts","../../src/collection/core/promptSafety.ts"],"sourcesContent":["// presentCollection tool — definition + pure server-side executor.\n//\n// Isomorphic (no Vue, no Node): bundled to the browser as the plugin's\n// `execute`, and run server-side via the host's plugin dispatch route. The\n// executor only validates + echoes the addressing; the live schema + items\n// are fetched client-side by the View through the host's /api/collections\n// routes, so a bad slug surfaces as the View's \"not found\" state.\n\nimport type { ToolContext, ToolDefinition, ToolResult } from \"gui-chat-protocol\";\n\nexport const TOOL_NAME = \"presentCollection\";\n\n/** Render payload carried in the tool result's `data` field; the View mounts\n * off these. Same shape as the tool args. */\nexport interface PresentCollectionData {\n /** Slug of the collection to display (e.g. \"clients\", \"invoices\"). */\n collectionSlug: string;\n /** Optional primary-key value of a single item to open on mount. */\n itemId?: string;\n}\n\nexport type PresentCollectionArgs = PresentCollectionData;\n\nexport const TOOL_DEFINITION: ToolDefinition = {\n type: \"function\",\n name: TOOL_NAME,\n description:\n \"Display a schema-driven collection inline in the chat as an interactive, editable card. Shows the collection's list of records. Pass `itemId` to open one specific record on mount.\",\n parameters: {\n type: \"object\",\n properties: {\n collectionSlug: {\n type: \"string\",\n description: \"The slug of the collection to display (e.g. 'clients', 'invoices', 'contacts').\",\n },\n itemId: {\n type: \"string\",\n description: \"Optional primary-key value of a single record to open in detail view on mount. Omit to show the full list.\",\n },\n },\n required: [\"collectionSlug\"],\n },\n prompt: `After making changes to schema-driven collections, use ${TOOL_NAME} to present either the collection or the item`,\n};\n\nexport const executePresentCollection = async (\n _context: ToolContext,\n args: PresentCollectionArgs,\n): Promise<ToolResult<PresentCollectionData, PresentCollectionData>> => {\n const collectionSlug = typeof args?.collectionSlug === \"string\" ? args.collectionSlug.trim() : \"\";\n if (!collectionSlug) {\n return {\n message: \"presentCollection error: collectionSlug is required\",\n instructions: \"Tell the user you couldn't display the collection because no collection was specified, and ask which collection they mean.\",\n };\n }\n const itemId = typeof args.itemId === \"string\" && args.itemId.trim().length > 0 ? args.itemId.trim() : undefined;\n const data: PresentCollectionData = itemId ? { collectionSlug, itemId } : { collectionSlug };\n const target = itemId ? `${collectionSlug} / ${itemId}` : collectionSlug;\n return {\n message: `Presented collection ${target}`,\n // `data` is the view's source (also the host's render-eligibility signal);\n // `jsonData` is what the LLM sees. Same payload, two audiences.\n data,\n jsonData: data,\n instructions:\n \"The collection has been presented to the user as an interactive card. They can browse, open, edit, create, and delete records directly. No further action is needed unless they ask.\",\n };\n};\n","// A standard, ordered colour palette for `enum` field values, shared by every\n// collection surface (list, calendar, kanban). Each value in an\n// enum's `values` array is assigned the palette entry at its index — cycling\n// when an enum declares more values than the palette holds — so colouring is\n// automatic and consistent without any per-value schema config.\n//\n// Tailwind only detects class names that appear as complete string literals,\n// so every surface's classes are spelled out per colour below rather than\n// built from a colour name at runtime.\n//\n// One enum is special: the field a schema's `notifyWhen` targets (the\n// \"notification enum\"). Its flagged values read the notification severity\n// colours — the first flagged value (most urgent) red, the rest amber — and\n// every other value reads neutral grey, mirroring the notification bell\n// (red = urgent, amber = nudge) rather than the rotating palette.\n// `resolveEnumColor` encapsulates that rule.\n\nimport type { CollectionSchema } from \"./schema\";\n\nexport interface EnumColorClasses {\n /** Stat-card style: border + fill + text + hover. */\n card: string;\n /** Small status dot (kanban column header). */\n dot: string;\n /** Pill / badge / inline `<select>` fill + text (no border width). */\n badge: string;\n /** Border colour, paired with a `border` width class by the caller. */\n border: string;\n}\n\n// The palette deliberately EXCLUDES the warm warning band (red / orange /\n// amber): those are reserved for the notification-enum severity colours\n// (`ENUM_ALERT` red, `ENUM_NUDGE` amber), so a normal enum value can never\n// draw a colour that reads like a notification. Eight well-separated cool /\n// green / magenta hues keep enough variety for wide enums.\nconst PALETTE: readonly EnumColorClasses[] = [\n {\n card: \"border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100\",\n dot: \"bg-indigo-500\",\n badge: \"bg-indigo-100 text-indigo-700\",\n border: \"border-indigo-200\",\n },\n { card: \"border-sky-200 bg-sky-50 text-sky-600 hover:bg-sky-100\", dot: \"bg-sky-500\", badge: \"bg-sky-100 text-sky-700\", border: \"border-sky-200\" },\n { card: \"border-cyan-200 bg-cyan-50 text-cyan-600 hover:bg-cyan-100\", dot: \"bg-cyan-500\", badge: \"bg-cyan-100 text-cyan-700\", border: \"border-cyan-200\" },\n { card: \"border-teal-200 bg-teal-50 text-teal-600 hover:bg-teal-100\", dot: \"bg-teal-500\", badge: \"bg-teal-100 text-teal-700\", border: \"border-teal-200\" },\n {\n card: \"border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100\",\n dot: \"bg-emerald-500\",\n badge: \"bg-emerald-100 text-emerald-700\",\n border: \"border-emerald-200\",\n },\n { card: \"border-lime-200 bg-lime-50 text-lime-600 hover:bg-lime-100\", dot: \"bg-lime-500\", badge: \"bg-lime-100 text-lime-700\", border: \"border-lime-200\" },\n {\n card: \"border-violet-200 bg-violet-50 text-violet-600 hover:bg-violet-100\",\n dot: \"bg-violet-500\",\n badge: \"bg-violet-100 text-violet-700\",\n border: \"border-violet-200\",\n },\n {\n card: \"border-fuchsia-200 bg-fuchsia-50 text-fuchsia-600 hover:bg-fuchsia-100\",\n dot: \"bg-fuchsia-500\",\n badge: \"bg-fuchsia-100 text-fuchsia-700\",\n border: \"border-fuchsia-200\",\n },\n];\n\n/** Neutral styling for the empty / Uncategorized bucket — never a palette\n * colour, so an unset or unknown value reads grey across every surface. */\nexport const ENUM_NEUTRAL: EnumColorClasses = {\n card: \"border-slate-200 bg-slate-50 text-slate-500 hover:bg-slate-100\",\n dot: \"bg-slate-300\",\n badge: \"bg-slate-100 text-slate-500\",\n border: \"border-slate-200\",\n};\n\n/** The urgent notification colour (red), matching the bell's `urgent`\n * severity. The first value a schema's `notifyWhen` flags reads this. */\nexport const ENUM_ALERT: EnumColorClasses = {\n card: \"border-red-200 bg-red-50 text-red-600 hover:bg-red-100\",\n dot: \"bg-red-500\",\n badge: \"bg-red-100 text-red-700\",\n border: \"border-red-200\",\n};\n\n/** The nudge notification colour (amber), matching the bell's `nudge`\n * severity. Flagged `notifyWhen` values after the first read this. */\nexport const ENUM_NUDGE: EnumColorClasses = {\n card: \"border-amber-200 bg-amber-50 text-amber-600 hover:bg-amber-100\",\n dot: \"bg-amber-500\",\n badge: \"bg-amber-100 text-amber-700\",\n border: \"border-amber-200\",\n};\n\n/** Classes for the enum value at `index` in its field's `values` array. A\n * negative index (value unset or not among the declared values) reads\n * neutral. */\nexport function enumColorClasses(index: number): EnumColorClasses {\n if (index < 0) return ENUM_NEUTRAL;\n return PALETTE[index % PALETTE.length] ?? ENUM_NEUTRAL;\n}\n\n/** Index of `value` within an enum field's declared `values`, or -1 when the\n * value is empty / unknown (→ neutral). */\nexport function enumValueIndex(values: readonly string[] | undefined, value: unknown): number {\n if (value === undefined || value === null || value === \"\") return -1;\n return values?.indexOf(String(value)) ?? -1;\n}\n\n/** The flagged values when `fieldKey` is the schema's `notifyWhen` target (the\n * \"notification enum\"); undefined for every other field. */\nfunction notifyValuesFor(schema: CollectionSchema, fieldKey: string): readonly string[] | undefined {\n const spec = schema.notifyWhen;\n return spec && spec.field === fieldKey ? spec.in : undefined;\n}\n\n/** Resolve a value's colour for enum field `fieldKey`:\n * - Notification enum (`notifyWhen` targets it): the first flagged value (in\n * `notifyWhen.in` order, the most urgent) reads notification red, the rest\n * amber, and every non-flagged value neutral grey.\n * - Any other enum: the standard palette by the value's declared index. */\nexport function resolveEnumColor(schema: CollectionSchema, fieldKey: string, value: unknown): EnumColorClasses {\n const notifyValues = notifyValuesFor(schema, fieldKey);\n if (notifyValues) {\n const str = value === undefined || value === null ? \"\" : String(value);\n const rank = notifyValues.indexOf(str);\n if (rank < 0) return ENUM_NEUTRAL;\n return rank === 0 ? ENUM_ALERT : ENUM_NUDGE;\n }\n const spec = schema.fields[fieldKey];\n return enumColorClasses(enumValueIndex(spec?.type === \"enum\" ? spec.values : undefined, value));\n}\n","// Pure `when`-predicate visibility helpers for schema-driven\n// collections — used both for action buttons and for conditionally\n// shown fields. Kept as their own module (no Vue) so CollectionView\n// can stay thin and the match semantics are pinned by unit tests.\n// Domain-free: the host compares the stringified record value against\n// the allowed set with no knowledge of what the field means.\n\n/** A `when` predicate: render only when the open record's `field`\n * (stringified) is one of `in`. Shared shape for action buttons and\n * conditionally visible fields. */\nexport interface WhenPredicate {\n field: string;\n in: string[];\n}\n\n/** Core matcher:\n * - no `when` ⇒ always true (visible);\n * - otherwise true only when `record[when.field]` is present and its\n * stringified value is one of `when.in`.\n * A missing/undefined/null field is treated as \"not a match\"\n * (hidden), so a status-gated target never shows on a record that\n * lacks the status. */\nexport function whenMatches(when: WhenPredicate | undefined, record: Record<string, unknown>): boolean {\n if (!when) return true;\n const value = record[when.field];\n if (value === undefined || value === null) return false;\n return when.in.includes(String(value));\n}\n\n/** Minimal shape this helper needs from an action — just its optional\n * `when` predicate. Accepts the full CollectionAction too. */\nexport interface ActionWithWhen {\n when?: WhenPredicate;\n}\n\n/** True when the action's button should render against `record`\n * (see whenMatches). */\nexport function actionVisible(action: ActionWithWhen, record: Record<string, unknown>): boolean {\n return whenMatches(action.when, record);\n}\n\n/** Minimal shape this helper needs from a field spec — just its\n * optional `when` predicate. Accepts the full FieldSpec too. */\nexport interface FieldWithWhen {\n when?: WhenPredicate;\n}\n\n/** True when the field should render against `record`. A field with\n * no `when` is always shown; otherwise it's shown only when the\n * record matches (e.g. hide a rating field until `visited` is true).\n * Purely presentational — a hidden field's stored value is never\n * altered, so toggling the gate back on restores it. */\nexport function fieldVisible(field: FieldWithWhen, record: Record<string, unknown>): boolean {\n return whenMatches(field.when, record);\n}\n","// Pure edit-draft logic for the collection record form, extracted from\n// CollectionView.vue so the parent (build draft / save / validate) and\n// the extracted CollectionRecordPanel (per-row table mutators) share one\n// implementation. No Vue, no I/O — every function maps a draft + schema\n// to a value, so the omission/validation semantics are unit-testable.\n\nimport { fieldVisible } from \"./actionVisible\";\nimport type { CollectionFieldSpec as FieldSpec, CollectionFieldType as FieldType, CollectionItem, CollectionSchema } from \"./schema\";\nimport type { EditState, TableRowDraft } from \"./uiTypes\";\n\n/** A fresh, empty row draft for a `table` field's sub-schema. */\nexport function emptyRow(subFields: Record<string, FieldSpec>): TableRowDraft {\n const text: Record<string, string> = {};\n const bool: Record<string, boolean> = {};\n const boolOriginallyPresent: Record<string, boolean> = {};\n const boolTouched: Record<string, boolean> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n if (subField.type === \"boolean\") {\n bool[subKey] = false;\n boolOriginallyPresent[subKey] = false; // brand-new row\n boolTouched[subKey] = false;\n } else {\n text[subKey] = \"\";\n }\n }\n return { text, bool, boolOriginallyPresent, boolTouched };\n}\n\n/** Build a row draft from an existing persisted row. */\nexport function rowFromItem(item: Record<string, unknown>, subFields: Record<string, FieldSpec>): TableRowDraft {\n const text: Record<string, string> = {};\n const bool: Record<string, boolean> = {};\n const boolOriginallyPresent: Record<string, boolean> = {};\n const boolTouched: Record<string, boolean> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n const raw = item[subKey];\n if (subField.type === \"boolean\") {\n bool[subKey] = raw === true;\n // `typeof raw === \"boolean\"` so an existing explicit `false` is\n // recorded as present and round-trips on a no-op save.\n boolOriginallyPresent[subKey] = typeof raw === \"boolean\";\n boolTouched[subKey] = false;\n } else {\n text[subKey] = raw === undefined || raw === null ? \"\" : String(raw);\n }\n }\n return { text, bool, boolOriginallyPresent, boolTouched };\n}\n\n/** Decide whether a boolean field's draft value should be emitted (vs.\n * omitted so a downstream default applies). */\nfunction shouldEmitBoolean(state: EditState, key: string, field: FieldSpec): boolean {\n return Boolean(state.boolOriginallyPresent[key] || state.boolTouched[key] || field.required);\n}\n\n/** Convert a scalar draft slot to its persisted form. `undefined` = omit. */\nfunction scalarDraftToValue(raw: string | undefined, fieldType: FieldType): unknown {\n if (raw === undefined || raw === \"\") return undefined;\n if (fieldType === \"number\" || fieldType === \"money\") {\n const num = Number(raw);\n return Number.isFinite(num) ? num : raw;\n }\n return raw;\n}\n\n/** Convert one table row draft to its persisted record. */\nfunction rowDraftToRecord(rowDraft: TableRowDraft, subFields: Record<string, FieldSpec>): Record<string, unknown> {\n const row: Record<string, unknown> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n if (subField.type === \"boolean\") {\n const value = rowDraft.bool[subKey] === true;\n if (rowDraft.boolOriginallyPresent[subKey] || rowDraft.boolTouched[subKey] || value || subField.required) row[subKey] = value;\n continue;\n }\n const value = scalarDraftToValue(rowDraft.text[subKey], subField.type);\n if (value !== undefined) row[subKey] = value;\n }\n return row;\n}\n\n/** Convert a full edit draft to the record to persist. */\nexport function draftToRecord(state: EditState, schema: CollectionSchema): CollectionItem {\n const record: CollectionItem = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n if (field.type === \"derived\" || field.type === \"embed\" || field.type === \"toggle\") continue; // never persisted (toggle projects an enum field)\n if (field.type === \"boolean\") {\n if (shouldEmitBoolean(state, key, field)) record[key] = state.bool[key] === true;\n continue;\n }\n if (field.type === \"table\" && field.of) {\n const subFields = field.of;\n record[key] = (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields));\n continue;\n }\n const value = scalarDraftToValue(state.text[key], field.type);\n if (value !== undefined) record[key] = value;\n }\n return record;\n}\n\n/** Normalise a raw inline-edit input (table-cell checkbox/select) to its\n * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty\n * enum selection (the placeholder option) clears the field via `undefined`. */\nexport function coerceInlineValue(field: FieldSpec, raw: boolean | string): unknown {\n if (field.type === \"boolean\") return raw === true;\n return raw === \"\" ? undefined : raw;\n}\n\n/** Build the full record to PUT for a single-cell inline edit, without\n * mutating `item`. A `undefined` value omits the key (enum cleared),\n * matching `draftToRecord`'s omission semantics. */\nexport function buildUpdatedRecord(item: CollectionItem, key: string, value: unknown): CollectionItem {\n if (value === undefined) {\n const { [key]: __omit, ...rest } = item;\n return rest;\n }\n return { ...item, [key]: value };\n}\n\n/** Empty for required-field validation — NOT a truthiness check (a\n * numeric `0` is a filled value). */\nfunction isMissingDraftValue(value: unknown): boolean {\n return value === undefined || value === null || value === \"\";\n}\n\n/** Label of the first required table sub-field empty in any row, else null. */\nfunction firstMissingTableSubField(field: FieldSpec, rows: TableRowDraft[] | undefined): string | null {\n if (field.type !== \"table\" || !rows) return null;\n for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {\n const row = rows[rowIdx];\n for (const [subKey, subField] of Object.entries(field.of)) {\n if (!subField.required || subField.type === \"boolean\") continue;\n if (isMissingDraftValue(row.text[subKey])) return `${field.label} #${rowIdx + 1}: ${subField.label}`;\n }\n }\n return null;\n}\n\nfunction validateOneField(key: string, field: FieldSpec, draft: EditState, record: CollectionItem): string | null {\n // A `when`-hidden field has no input the user can fill — never missing.\n if (!fieldVisible(field, record)) return null;\n if (field.type === \"table\" && field.of) {\n const rows = draft.table[key];\n if (field.required && (!rows || rows.length === 0)) return field.label;\n return firstMissingTableSubField(field, rows);\n }\n if (!field.required) return null;\n if (draft.mode === \"create\" && field.primary === true) return null; // server auto-generates id\n if (field.type === \"boolean\" || field.type === \"derived\" || field.type === \"embed\" || field.type === \"toggle\") return null;\n return isMissingDraftValue(draft.text[key]) ? field.label : null;\n}\n\n/** Human-readable label of the first missing required field, or null. */\nexport function firstMissingRequiredField(draft: EditState, schema: CollectionSchema): string | null {\n const record = draftToRecord(draft, schema);\n for (const [key, field] of Object.entries(schema.fields)) {\n const missing = validateOneField(key, field, draft, record);\n if (missing) return missing;\n }\n return null;\n}\n","// Sorting for the collection list table. The header sort toggle (next to\n// each field title) cycles a single active column through none → asc →\n// desc → none; this module turns that state plus a per-field value\n// extractor into an ordered copy of the rows.\n//\n// Two invariants the comparator guarantees:\n// 1. Rows with an empty/missing value for the sorted field always sink\n// to the bottom, regardless of direction.\n// 2. Ties (and the empty group) keep their original order — the sort is\n// stable via the captured source index.\n//\n// Field-type → comparable mapping (see `isSortableField`):\n// string/text/email → string · number/money → numeric ·\n// date/datetime → epoch-ms · enum → declared-index · boolean/toggle →\n// false<true · ref → display label · derived → its display type.\n// markdown/table/image/file/embed get no sort button.\n\nimport type { CollectionItem, CollectionFieldSpec, CollectionFieldType } from \"./schema\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n /** Field key of the single active sort column. */\n field: string;\n direction: SortDirection;\n}\n\n/** A row's comparable value for the active field. Exactly one of `num` /\n * `str` is set when not empty; `empty` rows always sort last. */\nexport interface SortValue {\n empty: boolean;\n num?: number;\n str?: string;\n}\n\nconst EMPTY: SortValue = { empty: true };\n\n/** Field types that render no value text in the table, so offer no sort. */\nconst NON_SORTABLE: ReadonlySet<CollectionFieldType> = new Set<CollectionFieldType>([\"markdown\", \"table\", \"image\", \"file\", \"embed\"]);\n\nexport function isSortableField(field: CollectionFieldSpec): boolean {\n return !NON_SORTABLE.has(field.type);\n}\n\n/** Cycle one column's state: none → asc → desc → none. */\nexport function nextSortDirection(current: SortDirection | null): SortDirection | null {\n if (current === null) return \"asc\";\n if (current === \"asc\") return \"desc\";\n return null;\n}\n\n// ── SortValue constructors (one per comparable kind) ────────────────\n\nexport function numericSortValue(raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const num = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(num) ? { empty: false, num } : EMPTY;\n}\n\nexport function stringSortValue(raw: unknown): SortValue {\n if (raw == null) return EMPTY;\n const str = String(raw);\n return str.trim() === \"\" ? EMPTY : { empty: false, str };\n}\n\nexport function dateSortValue(raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const epoch = Date.parse(String(raw));\n // Unparseable dates fall back to a lexical compare rather than vanishing.\n return Number.isNaN(epoch) ? stringSortValue(raw) : { empty: false, num: epoch };\n}\n\n/** Enum sorts by the value's index in the declared `values` list. A value\n * outside the list (or unset) is treated as empty → last. */\nexport function enumSortValue(values: readonly string[] | undefined, raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const idx = values ? values.indexOf(String(raw)) : -1;\n return idx < 0 ? EMPTY : { empty: false, num: idx };\n}\n\n/** Boolean / toggle: false < true, never empty (unset reads as false). */\nexport function boolSortValue(checked: boolean): SortValue {\n return { empty: false, num: checked ? 1 : 0 };\n}\n\n// ── Comparator + driver ─────────────────────────────────────────────\n\nexport function compareSortValues(left: SortValue, right: SortValue): number {\n if (left.num !== undefined && right.num !== undefined) return left.num - right.num;\n const leftStr = left.str ?? String(left.num ?? \"\");\n const rightStr = right.str ?? String(right.num ?? \"\");\n return leftStr.localeCompare(rightStr);\n}\n\n/** Stable sort of `items` by `valueOf`. Empties always last; ties hold\n * source order. Returns a new array (does not mutate `items`). */\nexport function sortItems(items: readonly CollectionItem[], direction: SortDirection, valueOf: (item: CollectionItem) => SortValue): CollectionItem[] {\n const dir = direction === \"asc\" ? 1 : -1;\n return items\n .map((item, index) => ({ item, index, sv: valueOf(item) }))\n .sort((left, right) => {\n if (left.sv.empty || right.sv.empty) {\n if (left.sv.empty && right.sv.empty) return left.index - right.index;\n return left.sv.empty ? 1 : -1;\n }\n const base = compareSortValues(left.sv, right.sv);\n return base !== 0 ? base * dir : left.index - right.index;\n })\n .map((decorated) => decorated.item);\n}\n","// Shared chip/label resolution for the collection calendar surfaces (month\n// grid + day view), so both label a record identically. Pure, type-only\n// dependency on the schema shape.\n\nimport type { CollectionItem, CollectionSchema } from \"./schema\";\n\n// Text-like field types that make a sensible human-readable label.\nconst LABEL_FIELD_TYPES = new Set([\"string\", \"text\", \"markdown\", \"email\"]);\n\n/** Which field labels a record: the schema's explicit `displayField`, else the\n * first non-primary text-like field (so a collection without a displayField\n * shows e.g. the title rather than the opaque primaryKey), else null → the\n * caller falls back to the primary-key value. */\nexport function labelFieldFor(schema: CollectionSchema): string | null {\n if (schema.displayField) return schema.displayField;\n for (const [key, spec] of Object.entries(schema.fields)) {\n if (key !== schema.primaryKey && LABEL_FIELD_TYPES.has(spec.type)) return key;\n }\n return null;\n}\n\n/** A record's primary-key value as a string. */\nexport function itemIdOf(item: CollectionItem, schema: CollectionSchema): string {\n return String(item[schema.primaryKey] ?? \"\");\n}\n\n/** A record's display label: the resolved `labelField` value, else the\n * primary key. Pass the result of `labelFieldFor(schema)` as `labelField`\n * (compute it once per render rather than per item). */\nexport function itemLabelOf(item: CollectionItem, schema: CollectionSchema, labelField: string | null): string {\n if (labelField) {\n const value = item[labelField];\n // Accept any primitive (string / number / boolean) so a numeric or boolean\n // display field still labels the record; objects/arrays fall through to the\n // id rather than rendering as \"[object Object]\".\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n const text = String(value);\n if (text.length > 0) return text;\n }\n }\n return itemIdOf(item, schema);\n}\n","/**\n * 8-char hex id — short, slug-safe, and editable. Produces the same id *shape*\n * as the server's `generateItemId()` (8 hex chars) so a UI-created collection\n * record looks like one the server would have generated for a form submitted\n * with a blank primary key. The source of randomness differs (UUID-derived here\n * vs `randomBytes` on the server); only the shape is intentionally shared.\n */\nexport function shortHexId(): string {\n return crypto.randomUUID().replace(/-/g, \"\").slice(0, 8);\n}\n","// Neutralize structural prompt-injection vectors in a short, record-derived\n// string before it rides into an LLM-facing prompt: strip angle brackets,\n// defang backticks / `${` template openings, collapse whitespace (so an\n// embedded newline can't fabricate a pseudo-instruction on its own line), and\n// clip to a small budget. Mirrors the host server's own defang so the two\n// can't drift (#1677).\n\n/** Max chars kept — the first batch of a validation issue is enough to act on. */\nconst DEFANG_MAX_LEN = 200;\n\nexport function defangForPrompt(value: string): string {\n return value.replace(/[<>]/g, \"\").replace(/`/g, \"'\").replace(/\\$\\{/g, \"$ {\").replace(/\\s+/g, \" \").slice(0, DEFANG_MAX_LEN);\n}\n"],"mappings":";;;AAUA,IAAa,YAAY;AAazB,IAAa,kBAAkC;CAC7C,MAAM;CACN,MAAM;CACN,aACE;CACF,YAAY;EACV,MAAM;EACN,YAAY;GACV,gBAAgB;IACd,MAAM;IACN,aAAa;GACf;GACA,QAAQ;IACN,MAAM;IACN,aAAa;GACf;EACF;EACA,UAAU,CAAC,gBAAgB;CAC7B;CACA,QAAQ,0DAA0D,UAAU;AAC9E;AAEA,IAAa,2BAA2B,OACtC,UACA,SACsE;CACtE,MAAM,iBAAiB,OAAO,MAAM,mBAAmB,WAAW,KAAK,eAAe,KAAK,IAAI;CAC/F,IAAI,CAAC,gBACH,OAAO;EACL,SAAS;EACT,cAAc;CAChB;CAEF,MAAM,SAAS,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,KAAK,CAAC,CAAC,SAAS,IAAI,KAAK,OAAO,KAAK,IAAI,KAAA;CACvG,MAAM,OAA8B,SAAS;EAAE;EAAgB;CAAO,IAAI,EAAE,eAAe;CAE3F,OAAO;EACL,SAAS,wBAFI,SAAS,GAAG,eAAe,KAAK,WAAW;EAKxD;EACA,UAAU;EACV,cACE;CACJ;AACF;;;ACjCA,IAAM,UAAuC;CAC3C;EACE,MAAM;EACN,KAAK;EACL,OAAO;EACP,QAAQ;CACV;CACA;EAAE,MAAM;EAA0D,KAAK;EAAc,OAAO;EAA2B,QAAQ;CAAiB;CAChJ;EAAE,MAAM;EAA8D,KAAK;EAAe,OAAO;EAA6B,QAAQ;CAAkB;CACxJ;EAAE,MAAM;EAA8D,KAAK;EAAe,OAAO;EAA6B,QAAQ;CAAkB;CACxJ;EACE,MAAM;EACN,KAAK;EACL,OAAO;EACP,QAAQ;CACV;CACA;EAAE,MAAM;EAA8D,KAAK;EAAe,OAAO;EAA6B,QAAQ;CAAkB;CACxJ;EACE,MAAM;EACN,KAAK;EACL,OAAO;EACP,QAAQ;CACV;CACA;EACE,MAAM;EACN,KAAK;EACL,OAAO;EACP,QAAQ;CACV;AACF;;;AAIA,IAAa,eAAiC;CAC5C,MAAM;CACN,KAAK;CACL,OAAO;CACP,QAAQ;AACV;;;AAIA,IAAa,aAA+B;CAC1C,MAAM;CACN,KAAK;CACL,OAAO;CACP,QAAQ;AACV;;;AAIA,IAAa,aAA+B;CAC1C,MAAM;CACN,KAAK;CACL,OAAO;CACP,QAAQ;AACV;;;;AAKA,SAAgB,iBAAiB,OAAiC;CAChE,IAAI,QAAQ,GAAG,OAAO;CACtB,OAAO,QAAQ,QAAQ,QAAQ,WAAW;AAC5C;;;AAIA,SAAgB,eAAe,QAAuC,OAAwB;CAC5F,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,IAAI,OAAO;CAClE,OAAO,QAAQ,QAAQ,OAAO,KAAK,CAAC,KAAK;AAC3C;;;AAIA,SAAS,gBAAgB,QAA0B,UAAiD;CAClG,MAAM,OAAO,OAAO;CACpB,OAAO,QAAQ,KAAK,UAAU,WAAW,KAAK,KAAK,KAAA;AACrD;;;;;;AAOA,SAAgB,iBAAiB,QAA0B,UAAkB,OAAkC;CAC7G,MAAM,eAAe,gBAAgB,QAAQ,QAAQ;CACrD,IAAI,cAAc;EAChB,MAAM,MAAM,UAAU,KAAA,KAAa,UAAU,OAAO,KAAK,OAAO,KAAK;EACrE,MAAM,OAAO,aAAa,QAAQ,GAAG;EACrC,IAAI,OAAO,GAAG,OAAO;EACrB,OAAO,SAAS,IAAI,aAAa;CACnC;CACA,MAAM,OAAO,OAAO,OAAO;CAC3B,OAAO,iBAAiB,eAAe,MAAM,SAAS,SAAS,KAAK,SAAS,KAAA,GAAW,KAAK,CAAC;AAChG;;;;;;;;;;AC5GA,SAAgB,YAAY,MAAiC,QAA0C;CACrG,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,QAAQ,OAAO,KAAK;CAC1B,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO;CAClD,OAAO,KAAK,GAAG,SAAS,OAAO,KAAK,CAAC;AACvC;;;AAUA,SAAgB,cAAc,QAAwB,QAA0C;CAC9F,OAAO,YAAY,OAAO,MAAM,MAAM;AACxC;;;;;;AAaA,SAAgB,aAAa,OAAsB,QAA0C;CAC3F,OAAO,YAAY,MAAM,MAAM,MAAM;AACvC;;;;AC3CA,SAAgB,SAAS,WAAqD;CAC5E,MAAM,OAA+B,CAAC;CACtC,MAAM,OAAgC,CAAC;CACvC,MAAM,wBAAiD,CAAC;CACxD,MAAM,cAAuC,CAAC;CAC9C,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,SAAS,GACvD,IAAI,SAAS,SAAS,WAAW;EAC/B,KAAK,UAAU;EACf,sBAAsB,UAAU;EAChC,YAAY,UAAU;CACxB,OACE,KAAK,UAAU;CAGnB,OAAO;EAAE;EAAM;EAAM;EAAuB;CAAY;AAC1D;;AAGA,SAAgB,YAAY,MAA+B,WAAqD;CAC9G,MAAM,OAA+B,CAAC;CACtC,MAAM,OAAgC,CAAC;CACvC,MAAM,wBAAiD,CAAC;CACxD,MAAM,cAAuC,CAAC;CAC9C,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,SAAS,GAAG;EAC1D,MAAM,MAAM,KAAK;EACjB,IAAI,SAAS,SAAS,WAAW;GAC/B,KAAK,UAAU,QAAQ;GAGvB,sBAAsB,UAAU,OAAO,QAAQ;GAC/C,YAAY,UAAU;EACxB,OACE,KAAK,UAAU,QAAQ,KAAA,KAAa,QAAQ,OAAO,KAAK,OAAO,GAAG;CAEtE;CACA,OAAO;EAAE;EAAM;EAAM;EAAuB;CAAY;AAC1D;;;AAIA,SAAS,kBAAkB,OAAkB,KAAa,OAA2B;CACnF,OAAO,QAAQ,MAAM,sBAAsB,QAAQ,MAAM,YAAY,QAAQ,MAAM,QAAQ;AAC7F;;AAGA,SAAS,mBAAmB,KAAyB,WAA+B;CAClF,IAAI,QAAQ,KAAA,KAAa,QAAQ,IAAI,OAAO,KAAA;CAC5C,IAAI,cAAc,YAAY,cAAc,SAAS;EACnD,MAAM,MAAM,OAAO,GAAG;EACtB,OAAO,OAAO,SAAS,GAAG,IAAI,MAAM;CACtC;CACA,OAAO;AACT;;AAGA,SAAS,iBAAiB,UAAyB,WAA+D;CAChH,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,SAAS,GAAG;EAC1D,IAAI,SAAS,SAAS,WAAW;GAC/B,MAAM,QAAQ,SAAS,KAAK,YAAY;GACxC,IAAI,SAAS,sBAAsB,WAAW,SAAS,YAAY,WAAW,SAAS,SAAS,UAAU,IAAI,UAAU;GACxH;EACF;EACA,MAAM,QAAQ,mBAAmB,SAAS,KAAK,SAAS,SAAS,IAAI;EACrE,IAAI,UAAU,KAAA,GAAW,IAAI,UAAU;CACzC;CACA,OAAO;AACT;;AAGA,SAAgB,cAAc,OAAkB,QAA0C;CACxF,MAAM,SAAyB,CAAC;CAChC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,MAAM,GAAG;EACxD,IAAI,MAAM,SAAS,aAAa,MAAM,SAAS,WAAW,MAAM,SAAS,UAAU;EACnF,IAAI,MAAM,SAAS,WAAW;GAC5B,IAAI,kBAAkB,OAAO,KAAK,KAAK,GAAG,OAAO,OAAO,MAAM,KAAK,SAAS;GAC5E;EACF;EACA,IAAI,MAAM,SAAS,WAAW,MAAM,IAAI;GACtC,MAAM,YAAY,MAAM;GACxB,OAAO,QAAQ,MAAM,MAAM,QAAQ,CAAC,EAAA,CAAG,KAAK,aAAa,iBAAiB,UAAU,SAAS,CAAC;GAC9F;EACF;EACA,MAAM,QAAQ,mBAAmB,MAAM,KAAK,MAAM,MAAM,IAAI;EAC5D,IAAI,UAAU,KAAA,GAAW,OAAO,OAAO;CACzC;CACA,OAAO;AACT;;;;AAKA,SAAgB,kBAAkB,OAAkB,KAAgC;CAClF,IAAI,MAAM,SAAS,WAAW,OAAO,QAAQ;CAC7C,OAAO,QAAQ,KAAK,KAAA,IAAY;AAClC;;;;AAKA,SAAgB,mBAAmB,MAAsB,KAAa,OAAgC;CACpG,IAAI,UAAU,KAAA,GAAW;EACvB,MAAM,GAAG,MAAM,QAAQ,GAAG,SAAS;EACnC,OAAO;CACT;CACA,OAAO;EAAE,GAAG;GAAO,MAAM;CAAM;AACjC;;;AAIA,SAAS,oBAAoB,OAAyB;CACpD,OAAO,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU;AAC5D;;AAGA,SAAS,0BAA0B,OAAkB,MAAkD;CACrG,IAAI,MAAM,SAAS,WAAW,CAAC,MAAM,OAAO;CAC5C,KAAK,IAAI,SAAS,GAAG,SAAS,KAAK,QAAQ,UAAU;EACnD,MAAM,MAAM,KAAK;EACjB,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,MAAM,EAAE,GAAG;GACzD,IAAI,CAAC,SAAS,YAAY,SAAS,SAAS,WAAW;GACvD,IAAI,oBAAoB,IAAI,KAAK,OAAO,GAAG,OAAO,GAAG,MAAM,MAAM,IAAI,SAAS,EAAE,IAAI,SAAS;EAC/F;CACF;CACA,OAAO;AACT;AAEA,SAAS,iBAAiB,KAAa,OAAkB,OAAkB,QAAuC;CAEhH,IAAI,CAAC,aAAa,OAAO,MAAM,GAAG,OAAO;CACzC,IAAI,MAAM,SAAS,WAAW,MAAM,IAAI;EACtC,MAAM,OAAO,MAAM,MAAM;EACzB,IAAI,MAAM,aAAa,CAAC,QAAQ,KAAK,WAAW,IAAI,OAAO,MAAM;EACjE,OAAO,0BAA0B,OAAO,IAAI;CAC9C;CACA,IAAI,CAAC,MAAM,UAAU,OAAO;CAC5B,IAAI,MAAM,SAAS,YAAY,MAAM,YAAY,MAAM,OAAO;CAC9D,IAAI,MAAM,SAAS,aAAa,MAAM,SAAS,aAAa,MAAM,SAAS,WAAW,MAAM,SAAS,UAAU,OAAO;CACtH,OAAO,oBAAoB,MAAM,KAAK,IAAI,IAAI,MAAM,QAAQ;AAC9D;;AAGA,SAAgB,0BAA0B,OAAkB,QAAyC;CACnG,MAAM,SAAS,cAAc,OAAO,MAAM;CAC1C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,MAAM,GAAG;EACxD,MAAM,UAAU,iBAAiB,KAAK,OAAO,OAAO,MAAM;EAC1D,IAAI,SAAS,OAAO;CACtB;CACA,OAAO;AACT;;;AC7HA,IAAM,QAAmB,EAAE,OAAO,KAAK;;AAGvC,IAAM,+BAAiD,IAAI,IAAyB;CAAC;CAAY;CAAS;CAAS;CAAQ;AAAO,CAAC;AAEnI,SAAgB,gBAAgB,OAAqC;CACnE,OAAO,CAAC,aAAa,IAAI,MAAM,IAAI;AACrC;;AAGA,SAAgB,kBAAkB,SAAqD;CACrF,IAAI,YAAY,MAAM,OAAO;CAC7B,IAAI,YAAY,OAAO,OAAO;CAC9B,OAAO;AACT;AAIA,SAAgB,iBAAiB,KAAyB;CACxD,IAAI,OAAO,QAAQ,QAAQ,IAAI,OAAO;CACtC,MAAM,MAAM,OAAO,QAAQ,WAAW,MAAM,OAAO,GAAG;CACtD,OAAO,OAAO,SAAS,GAAG,IAAI;EAAE,OAAO;EAAO;CAAI,IAAI;AACxD;AAEA,SAAgB,gBAAgB,KAAyB;CACvD,IAAI,OAAO,MAAM,OAAO;CACxB,MAAM,MAAM,OAAO,GAAG;CACtB,OAAO,IAAI,KAAK,MAAM,KAAK,QAAQ;EAAE,OAAO;EAAO;CAAI;AACzD;AAEA,SAAgB,cAAc,KAAyB;CACrD,IAAI,OAAO,QAAQ,QAAQ,IAAI,OAAO;CACtC,MAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,CAAC;CAEpC,OAAO,OAAO,MAAM,KAAK,IAAI,gBAAgB,GAAG,IAAI;EAAE,OAAO;EAAO,KAAK;CAAM;AACjF;;;AAIA,SAAgB,cAAc,QAAuC,KAAyB;CAC5F,IAAI,OAAO,QAAQ,QAAQ,IAAI,OAAO;CACtC,MAAM,MAAM,SAAS,OAAO,QAAQ,OAAO,GAAG,CAAC,IAAI;CACnD,OAAO,MAAM,IAAI,QAAQ;EAAE,OAAO;EAAO,KAAK;CAAI;AACpD;;AAGA,SAAgB,cAAc,SAA6B;CACzD,OAAO;EAAE,OAAO;EAAO,KAAK,UAAU,IAAI;CAAE;AAC9C;AAIA,SAAgB,kBAAkB,MAAiB,OAA0B;CAC3E,IAAI,KAAK,QAAQ,KAAA,KAAa,MAAM,QAAQ,KAAA,GAAW,OAAO,KAAK,MAAM,MAAM;CAC/E,MAAM,UAAU,KAAK,OAAO,OAAO,KAAK,OAAO,EAAE;CACjD,MAAM,WAAW,MAAM,OAAO,OAAO,MAAM,OAAO,EAAE;CACpD,OAAO,QAAQ,cAAc,QAAQ;AACvC;;;AAIA,SAAgB,UAAU,OAAkC,WAA0B,SAAgE;CACpJ,MAAM,MAAM,cAAc,QAAQ,IAAI;CACtC,OAAO,MACJ,KAAK,MAAM,WAAW;EAAE;EAAM;EAAO,IAAI,QAAQ,IAAI;CAAE,EAAE,CAAC,CAC1D,MAAM,MAAM,UAAU;EACrB,IAAI,KAAK,GAAG,SAAS,MAAM,GAAG,OAAO;GACnC,IAAI,KAAK,GAAG,SAAS,MAAM,GAAG,OAAO,OAAO,KAAK,QAAQ,MAAM;GAC/D,OAAO,KAAK,GAAG,QAAQ,IAAI;EAC7B;EACA,MAAM,OAAO,kBAAkB,KAAK,IAAI,MAAM,EAAE;EAChD,OAAO,SAAS,IAAI,OAAO,MAAM,KAAK,QAAQ,MAAM;CACtD,CAAC,CAAC,CACD,KAAK,cAAc,UAAU,IAAI;AACtC;;;ACtGA,IAAM,oCAAoB,IAAI,IAAI;CAAC;CAAU;CAAQ;CAAY;AAAO,CAAC;;;;;AAMzE,SAAgB,cAAc,QAAyC;CACrE,IAAI,OAAO,cAAc,OAAO,OAAO;CACvC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,OAAO,MAAM,GACpD,IAAI,QAAQ,OAAO,cAAc,kBAAkB,IAAI,KAAK,IAAI,GAAG,OAAO;CAE5E,OAAO;AACT;;AAGA,SAAgB,SAAS,MAAsB,QAAkC;CAC/E,OAAO,OAAO,KAAK,OAAO,eAAe,EAAE;AAC7C;;;;AAKA,SAAgB,YAAY,MAAsB,QAA0B,YAAmC;CAC7G,IAAI,YAAY;EACd,MAAM,QAAQ,KAAK;EAInB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;GACxF,MAAM,OAAO,OAAO,KAAK;GACzB,IAAI,KAAK,SAAS,GAAG,OAAO;EAC9B;CACF;CACA,OAAO,SAAS,MAAM,MAAM;AAC9B;;;;;;;;;;AClCA,SAAgB,aAAqB;CACnC,OAAO,OAAO,WAAW,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzD;;;;ACDA,IAAM,iBAAiB;AAEvB,SAAgB,gBAAgB,OAAuB;CACrD,OAAO,MAAM,QAAQ,SAAS,EAAE,CAAC,CAAC,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,SAAS,KAAK,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,MAAM,GAAG,cAAc;AAC3H"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/collection/core/presentCollection.ts","../../src/collection/core/enumColors.ts","../../src/collection/core/draft.ts","../../src/collection/core/sortItems.ts","../../src/collection/core/itemLabel.ts","../../src/collection/core/shortHexId.ts","../../src/collection/core/promptSafety.ts"],"sourcesContent":["// presentCollection tool — definition + pure server-side executor.\n//\n// Isomorphic (no Vue, no Node): bundled to the browser as the plugin's\n// `execute`, and run server-side via the host's plugin dispatch route. The\n// executor only validates + echoes the addressing; the live schema + items\n// are fetched client-side by the View through the host's /api/collections\n// routes, so a bad slug surfaces as the View's \"not found\" state.\n\nimport type { ToolContext, ToolDefinition, ToolResult } from \"gui-chat-protocol\";\n\nexport const TOOL_NAME = \"presentCollection\";\n\n/** Render payload carried in the tool result's `data` field; the View mounts\n * off these. Same shape as the tool args. */\nexport interface PresentCollectionData {\n /** Slug of the collection to display (e.g. \"clients\", \"invoices\"). */\n collectionSlug: string;\n /** Optional primary-key value of a single item to open on mount. */\n itemId?: string;\n}\n\nexport type PresentCollectionArgs = PresentCollectionData;\n\nexport const TOOL_DEFINITION: ToolDefinition = {\n type: \"function\",\n name: TOOL_NAME,\n description:\n \"Display a schema-driven collection inline in the chat as an interactive, editable card. Shows the collection's list of records. Pass `itemId` to open one specific record on mount.\",\n parameters: {\n type: \"object\",\n properties: {\n collectionSlug: {\n type: \"string\",\n description: \"The slug of the collection to display (e.g. 'clients', 'invoices', 'contacts').\",\n },\n itemId: {\n type: \"string\",\n description: \"Optional primary-key value of a single record to open in detail view on mount. Omit to show the full list.\",\n },\n },\n required: [\"collectionSlug\"],\n },\n prompt: `After making changes to schema-driven collections, use ${TOOL_NAME} to present either the collection or the item`,\n};\n\nexport const executePresentCollection = async (\n _context: ToolContext,\n args: PresentCollectionArgs,\n): Promise<ToolResult<PresentCollectionData, PresentCollectionData>> => {\n const collectionSlug = typeof args?.collectionSlug === \"string\" ? args.collectionSlug.trim() : \"\";\n if (!collectionSlug) {\n return {\n message: \"presentCollection error: collectionSlug is required\",\n instructions: \"Tell the user you couldn't display the collection because no collection was specified, and ask which collection they mean.\",\n };\n }\n const itemId = typeof args.itemId === \"string\" && args.itemId.trim().length > 0 ? args.itemId.trim() : undefined;\n const data: PresentCollectionData = itemId ? { collectionSlug, itemId } : { collectionSlug };\n const target = itemId ? `${collectionSlug} / ${itemId}` : collectionSlug;\n return {\n message: `Presented collection ${target}`,\n // `data` is the view's source (also the host's render-eligibility signal);\n // `jsonData` is what the LLM sees. Same payload, two audiences.\n data,\n jsonData: data,\n instructions:\n \"The collection has been presented to the user as an interactive card. They can browse, open, edit, create, and delete records directly. No further action is needed unless they ask.\",\n };\n};\n","// A standard, ordered colour palette for `enum` field values, shared by every\n// collection surface (list, calendar, kanban). Each value in an\n// enum's `values` array is assigned the palette entry at its index — cycling\n// when an enum declares more values than the palette holds — so colouring is\n// automatic and consistent without any per-value schema config.\n//\n// Tailwind only detects class names that appear as complete string literals,\n// so every surface's classes are spelled out per colour below rather than\n// built from a colour name at runtime.\n//\n// One enum is special: the field a schema's `notifyWhen` targets (the\n// \"notification enum\"). Its flagged values read the notification severity\n// colours — the first flagged value (most urgent) red, the rest amber — and\n// every other value reads neutral grey, mirroring the notification bell\n// (red = urgent, amber = nudge) rather than the rotating palette.\n// `resolveEnumColor` encapsulates that rule.\n\nimport type { CollectionSchema } from \"./schema\";\n\nexport interface EnumColorClasses {\n /** Stat-card style: border + fill + text + hover. */\n card: string;\n /** Small status dot (kanban column header). */\n dot: string;\n /** Pill / badge / inline `<select>` fill + text (no border width). */\n badge: string;\n /** Border colour, paired with a `border` width class by the caller. */\n border: string;\n}\n\n// The palette deliberately EXCLUDES the warm warning band (red / orange /\n// amber): those are reserved for the notification-enum severity colours\n// (`ENUM_ALERT` red, `ENUM_NUDGE` amber), so a normal enum value can never\n// draw a colour that reads like a notification. Eight well-separated cool /\n// green / magenta hues keep enough variety for wide enums.\nconst PALETTE: readonly EnumColorClasses[] = [\n {\n card: \"border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100\",\n dot: \"bg-indigo-500\",\n badge: \"bg-indigo-100 text-indigo-700\",\n border: \"border-indigo-200\",\n },\n { card: \"border-sky-200 bg-sky-50 text-sky-600 hover:bg-sky-100\", dot: \"bg-sky-500\", badge: \"bg-sky-100 text-sky-700\", border: \"border-sky-200\" },\n { card: \"border-cyan-200 bg-cyan-50 text-cyan-600 hover:bg-cyan-100\", dot: \"bg-cyan-500\", badge: \"bg-cyan-100 text-cyan-700\", border: \"border-cyan-200\" },\n { card: \"border-teal-200 bg-teal-50 text-teal-600 hover:bg-teal-100\", dot: \"bg-teal-500\", badge: \"bg-teal-100 text-teal-700\", border: \"border-teal-200\" },\n {\n card: \"border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100\",\n dot: \"bg-emerald-500\",\n badge: \"bg-emerald-100 text-emerald-700\",\n border: \"border-emerald-200\",\n },\n { card: \"border-lime-200 bg-lime-50 text-lime-600 hover:bg-lime-100\", dot: \"bg-lime-500\", badge: \"bg-lime-100 text-lime-700\", border: \"border-lime-200\" },\n {\n card: \"border-violet-200 bg-violet-50 text-violet-600 hover:bg-violet-100\",\n dot: \"bg-violet-500\",\n badge: \"bg-violet-100 text-violet-700\",\n border: \"border-violet-200\",\n },\n {\n card: \"border-fuchsia-200 bg-fuchsia-50 text-fuchsia-600 hover:bg-fuchsia-100\",\n dot: \"bg-fuchsia-500\",\n badge: \"bg-fuchsia-100 text-fuchsia-700\",\n border: \"border-fuchsia-200\",\n },\n];\n\n/** Neutral styling for the empty / Uncategorized bucket — never a palette\n * colour, so an unset or unknown value reads grey across every surface. */\nexport const ENUM_NEUTRAL: EnumColorClasses = {\n card: \"border-slate-200 bg-slate-50 text-slate-500 hover:bg-slate-100\",\n dot: \"bg-slate-300\",\n badge: \"bg-slate-100 text-slate-500\",\n border: \"border-slate-200\",\n};\n\n/** The urgent notification colour (red), matching the bell's `urgent`\n * severity. The first value a schema's `notifyWhen` flags reads this. */\nexport const ENUM_ALERT: EnumColorClasses = {\n card: \"border-red-200 bg-red-50 text-red-600 hover:bg-red-100\",\n dot: \"bg-red-500\",\n badge: \"bg-red-100 text-red-700\",\n border: \"border-red-200\",\n};\n\n/** The nudge notification colour (amber), matching the bell's `nudge`\n * severity. Flagged `notifyWhen` values after the first read this. */\nexport const ENUM_NUDGE: EnumColorClasses = {\n card: \"border-amber-200 bg-amber-50 text-amber-600 hover:bg-amber-100\",\n dot: \"bg-amber-500\",\n badge: \"bg-amber-100 text-amber-700\",\n border: \"border-amber-200\",\n};\n\n/** Classes for the enum value at `index` in its field's `values` array. A\n * negative index (value unset or not among the declared values) reads\n * neutral. */\nexport function enumColorClasses(index: number): EnumColorClasses {\n if (index < 0) return ENUM_NEUTRAL;\n return PALETTE[index % PALETTE.length] ?? ENUM_NEUTRAL;\n}\n\n/** Index of `value` within an enum field's declared `values`, or -1 when the\n * value is empty / unknown (→ neutral). */\nexport function enumValueIndex(values: readonly string[] | undefined, value: unknown): number {\n if (value === undefined || value === null || value === \"\") return -1;\n return values?.indexOf(String(value)) ?? -1;\n}\n\n/** The flagged values when `fieldKey` is the schema's `notifyWhen` target (the\n * \"notification enum\"); undefined for every other field. */\nfunction notifyValuesFor(schema: CollectionSchema, fieldKey: string): readonly string[] | undefined {\n const spec = schema.notifyWhen;\n return spec && spec.field === fieldKey ? spec.in : undefined;\n}\n\n/** Resolve a value's colour for enum field `fieldKey`:\n * - Notification enum (`notifyWhen` targets it): the first flagged value (in\n * `notifyWhen.in` order, the most urgent) reads notification red, the rest\n * amber, and every non-flagged value neutral grey.\n * - Any other enum: the standard palette by the value's declared index. */\nexport function resolveEnumColor(schema: CollectionSchema, fieldKey: string, value: unknown): EnumColorClasses {\n const notifyValues = notifyValuesFor(schema, fieldKey);\n if (notifyValues) {\n const str = value === undefined || value === null ? \"\" : String(value);\n const rank = notifyValues.indexOf(str);\n if (rank < 0) return ENUM_NEUTRAL;\n return rank === 0 ? ENUM_ALERT : ENUM_NUDGE;\n }\n const spec = schema.fields[fieldKey];\n return enumColorClasses(enumValueIndex(spec?.type === \"enum\" ? spec.values : undefined, value));\n}\n","// Pure edit-draft logic for the collection record form, extracted from\n// CollectionView.vue so the parent (build draft / save / validate) and\n// the extracted CollectionRecordPanel (per-row table mutators) share one\n// implementation. No Vue, no I/O — every function maps a draft + schema\n// to a value, so the omission/validation semantics are unit-testable.\n\nimport { fieldVisible } from \"./actionVisible\";\nimport { COMPUTED_TYPES } from \"./schema\";\nimport type { CollectionFieldSpec as FieldSpec, CollectionFieldType as FieldType, CollectionItem, CollectionSchema } from \"./schema\";\nimport type { EditState, TableRowDraft } from \"./uiTypes\";\n\n/** A fresh, empty row draft for a `table` field's sub-schema. */\nexport function emptyRow(subFields: Record<string, FieldSpec>): TableRowDraft {\n const text: Record<string, string> = {};\n const bool: Record<string, boolean> = {};\n const boolOriginallyPresent: Record<string, boolean> = {};\n const boolTouched: Record<string, boolean> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n if (subField.type === \"boolean\") {\n bool[subKey] = false;\n boolOriginallyPresent[subKey] = false; // brand-new row\n boolTouched[subKey] = false;\n } else {\n text[subKey] = \"\";\n }\n }\n return { text, bool, boolOriginallyPresent, boolTouched };\n}\n\n/** Build a row draft from an existing persisted row. */\nexport function rowFromItem(item: Record<string, unknown>, subFields: Record<string, FieldSpec>): TableRowDraft {\n const text: Record<string, string> = {};\n const bool: Record<string, boolean> = {};\n const boolOriginallyPresent: Record<string, boolean> = {};\n const boolTouched: Record<string, boolean> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n const raw = item[subKey];\n if (subField.type === \"boolean\") {\n bool[subKey] = raw === true;\n // `typeof raw === \"boolean\"` so an existing explicit `false` is\n // recorded as present and round-trips on a no-op save.\n boolOriginallyPresent[subKey] = typeof raw === \"boolean\";\n boolTouched[subKey] = false;\n } else {\n text[subKey] = raw === undefined || raw === null ? \"\" : String(raw);\n }\n }\n return { text, bool, boolOriginallyPresent, boolTouched };\n}\n\n/** Decide whether a boolean field's draft value should be emitted (vs.\n * omitted so a downstream default applies). */\nfunction shouldEmitBoolean(state: EditState, key: string, field: FieldSpec): boolean {\n return Boolean(state.boolOriginallyPresent[key] || state.boolTouched[key] || field.required);\n}\n\n/** Convert a scalar draft slot to its persisted form. `undefined` = omit. */\nfunction scalarDraftToValue(raw: string | undefined, fieldType: FieldType): unknown {\n if (raw === undefined || raw === \"\") return undefined;\n if (fieldType === \"number\" || fieldType === \"money\") {\n const num = Number(raw);\n return Number.isFinite(num) ? num : raw;\n }\n return raw;\n}\n\n/** Convert one table row draft to its persisted record. */\nfunction rowDraftToRecord(rowDraft: TableRowDraft, subFields: Record<string, FieldSpec>): Record<string, unknown> {\n const row: Record<string, unknown> = {};\n for (const [subKey, subField] of Object.entries(subFields)) {\n if (subField.type === \"boolean\") {\n const value = rowDraft.bool[subKey] === true;\n if (rowDraft.boolOriginallyPresent[subKey] || rowDraft.boolTouched[subKey] || value || subField.required) row[subKey] = value;\n continue;\n }\n const value = scalarDraftToValue(rowDraft.text[subKey], subField.type);\n if (value !== undefined) row[subKey] = value;\n }\n return row;\n}\n\n/** Convert a full edit draft to the record to persist. */\nexport function draftToRecord(state: EditState, schema: CollectionSchema): CollectionItem {\n const record: CollectionItem = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n if (COMPUTED_TYPES.has(field.type)) continue; // never persisted (toggle projects an enum field)\n if (field.type === \"boolean\") {\n if (shouldEmitBoolean(state, key, field)) record[key] = state.bool[key] === true;\n continue;\n }\n if (field.type === \"table\" && field.of) {\n const subFields = field.of;\n record[key] = (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields));\n continue;\n }\n const value = scalarDraftToValue(state.text[key], field.type);\n if (value !== undefined) record[key] = value;\n }\n return record;\n}\n\n/** Normalise a raw inline-edit input (table-cell checkbox/select) to its\n * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty\n * enum selection (the placeholder option) clears the field via `undefined`. */\nexport function coerceInlineValue(field: FieldSpec, raw: boolean | string): unknown {\n if (field.type === \"boolean\") return raw === true;\n return raw === \"\" ? undefined : raw;\n}\n\n/** Build the full record to PUT for a single-cell inline edit, without\n * mutating `item`. A `undefined` value omits the key (enum cleared),\n * matching `draftToRecord`'s omission semantics. */\nexport function buildUpdatedRecord(item: CollectionItem, key: string, value: unknown): CollectionItem {\n if (value === undefined) {\n const { [key]: __omit, ...rest } = item;\n return rest;\n }\n return { ...item, [key]: value };\n}\n\n/** Empty for required-field validation — NOT a truthiness check (a\n * numeric `0` is a filled value). */\nfunction isMissingDraftValue(value: unknown): boolean {\n return value === undefined || value === null || value === \"\";\n}\n\n/** Label of the first required table sub-field empty in any row, else null. */\nfunction firstMissingTableSubField(field: FieldSpec, rows: TableRowDraft[] | undefined): string | null {\n if (field.type !== \"table\" || !rows) return null;\n for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {\n const row = rows[rowIdx];\n for (const [subKey, subField] of Object.entries(field.of)) {\n if (!subField.required || subField.type === \"boolean\") continue;\n if (isMissingDraftValue(row.text[subKey])) return `${field.label} #${rowIdx + 1}: ${subField.label}`;\n }\n }\n return null;\n}\n\n/** Field types the required check never flags: booleans always hold a\n * value, and the computed/projected kinds (COMPUTED_TYPES — the shared\n * set, so a future computed type can't drift past this check) have no\n * draft input at all. */\nconst NEVER_MISSING_TYPES: ReadonlySet<FieldType> = new Set<FieldType>([\"boolean\", ...COMPUTED_TYPES]);\n\nfunction validateOneField(key: string, field: FieldSpec, draft: EditState, record: CollectionItem): string | null {\n // A `when`-hidden field has no input the user can fill — never missing.\n if (!fieldVisible(field, record)) return null;\n if (field.type === \"table\" && field.of) {\n const rows = draft.table[key];\n if (field.required && (!rows || rows.length === 0)) return field.label;\n return firstMissingTableSubField(field, rows);\n }\n if (!field.required) return null;\n if (draft.mode === \"create\" && field.primary === true) return null; // server auto-generates id\n if (NEVER_MISSING_TYPES.has(field.type)) return null;\n return isMissingDraftValue(draft.text[key]) ? field.label : null;\n}\n\n/** Human-readable label of the first missing required field, or null. */\nexport function firstMissingRequiredField(draft: EditState, schema: CollectionSchema): string | null {\n const record = draftToRecord(draft, schema);\n for (const [key, field] of Object.entries(schema.fields)) {\n const missing = validateOneField(key, field, draft, record);\n if (missing) return missing;\n }\n return null;\n}\n","// Sorting for the collection list table. The header sort toggle (next to\n// each field title) cycles a single active column through none → asc →\n// desc → none; this module turns that state plus a per-field value\n// extractor into an ordered copy of the rows.\n//\n// Two invariants the comparator guarantees:\n// 1. Rows with an empty/missing value for the sorted field always sink\n// to the bottom, regardless of direction.\n// 2. Ties (and the empty group) keep their original order — the sort is\n// stable via the captured source index.\n//\n// Field-type → comparable mapping (see `isSortableField`):\n// string/text/email → string · number/money → numeric ·\n// date/datetime → epoch-ms · enum → declared-index · boolean/toggle →\n// false<true · ref → display label · derived → its display type.\n// markdown/table/image/file/embed get no sort button.\n\nimport type { CollectionItem, CollectionFieldSpec, CollectionFieldType } from \"./schema\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n /** Field key of the single active sort column. */\n field: string;\n direction: SortDirection;\n}\n\n/** A row's comparable value for the active field. Exactly one of `num` /\n * `str` is set when not empty; `empty` rows always sort last. */\nexport interface SortValue {\n empty: boolean;\n num?: number;\n str?: string;\n}\n\nconst EMPTY: SortValue = { empty: true };\n\n/** Field types that render no value text in the table, so offer no sort. */\nconst NON_SORTABLE: ReadonlySet<CollectionFieldType> = new Set<CollectionFieldType>([\"markdown\", \"table\", \"image\", \"file\", \"embed\", \"backlinks\", \"rollup\"]);\n\nexport function isSortableField(field: CollectionFieldSpec): boolean {\n return !NON_SORTABLE.has(field.type);\n}\n\n/** Cycle one column's state: none → asc → desc → none. */\nexport function nextSortDirection(current: SortDirection | null): SortDirection | null {\n if (current === null) return \"asc\";\n if (current === \"asc\") return \"desc\";\n return null;\n}\n\n// ── SortValue constructors (one per comparable kind) ────────────────\n\nexport function numericSortValue(raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const num = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(num) ? { empty: false, num } : EMPTY;\n}\n\nexport function stringSortValue(raw: unknown): SortValue {\n if (raw == null) return EMPTY;\n const str = String(raw);\n return str.trim() === \"\" ? EMPTY : { empty: false, str };\n}\n\nexport function dateSortValue(raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const epoch = Date.parse(String(raw));\n // Unparseable dates fall back to a lexical compare rather than vanishing.\n return Number.isNaN(epoch) ? stringSortValue(raw) : { empty: false, num: epoch };\n}\n\n/** Enum sorts by the value's index in the declared `values` list. A value\n * outside the list (or unset) is treated as empty → last. */\nexport function enumSortValue(values: readonly string[] | undefined, raw: unknown): SortValue {\n if (raw == null || raw === \"\") return EMPTY;\n const idx = values ? values.indexOf(String(raw)) : -1;\n return idx < 0 ? EMPTY : { empty: false, num: idx };\n}\n\n/** Boolean / toggle: false < true, never empty (unset reads as false). */\nexport function boolSortValue(checked: boolean): SortValue {\n return { empty: false, num: checked ? 1 : 0 };\n}\n\n// ── Comparator + driver ─────────────────────────────────────────────\n\nexport function compareSortValues(left: SortValue, right: SortValue): number {\n if (left.num !== undefined && right.num !== undefined) return left.num - right.num;\n const leftStr = left.str ?? String(left.num ?? \"\");\n const rightStr = right.str ?? String(right.num ?? \"\");\n return leftStr.localeCompare(rightStr);\n}\n\n/** Stable sort of `items` by `valueOf`. Empties always last; ties hold\n * source order. Returns a new array (does not mutate `items`). */\nexport function sortItems(items: readonly CollectionItem[], direction: SortDirection, valueOf: (item: CollectionItem) => SortValue): CollectionItem[] {\n const dir = direction === \"asc\" ? 1 : -1;\n return items\n .map((item, index) => ({ item, index, sv: valueOf(item) }))\n .sort((left, right) => {\n if (left.sv.empty || right.sv.empty) {\n if (left.sv.empty && right.sv.empty) return left.index - right.index;\n return left.sv.empty ? 1 : -1;\n }\n const base = compareSortValues(left.sv, right.sv);\n return base !== 0 ? base * dir : left.index - right.index;\n })\n .map((decorated) => decorated.item);\n}\n","// Shared chip/label resolution for the collection calendar surfaces (month\n// grid + day view), so both label a record identically. Pure, type-only\n// dependency on the schema shape.\n\nimport type { CollectionItem, CollectionSchema } from \"./schema\";\n\n// Text-like field types that make a sensible human-readable label.\nconst LABEL_FIELD_TYPES = new Set([\"string\", \"text\", \"markdown\", \"email\"]);\n\n/** Which field labels a record: the schema's explicit `displayField`, else the\n * first non-primary text-like field (so a collection without a displayField\n * shows e.g. the title rather than the opaque primaryKey), else null → the\n * caller falls back to the primary-key value. */\nexport function labelFieldFor(schema: CollectionSchema): string | null {\n if (schema.displayField) return schema.displayField;\n for (const [key, spec] of Object.entries(schema.fields)) {\n if (key !== schema.primaryKey && LABEL_FIELD_TYPES.has(spec.type)) return key;\n }\n return null;\n}\n\n/** A record's primary-key value as a string. */\nexport function itemIdOf(item: CollectionItem, schema: CollectionSchema): string {\n return String(item[schema.primaryKey] ?? \"\");\n}\n\n/** A record's display label: the resolved `labelField` value, else the\n * primary key. Pass the result of `labelFieldFor(schema)` as `labelField`\n * (compute it once per render rather than per item). */\nexport function itemLabelOf(item: CollectionItem, schema: CollectionSchema, labelField: string | null): string {\n if (labelField) {\n const value = item[labelField];\n // Accept any primitive (string / number / boolean) so a numeric or boolean\n // display field still labels the record; objects/arrays fall through to the\n // id rather than rendering as \"[object Object]\".\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n const text = String(value);\n if (text.length > 0) return text;\n }\n }\n return itemIdOf(item, schema);\n}\n","/**\n * 8-char hex id — short, slug-safe, and editable. Produces the same id *shape*\n * as the server's `generateItemId()` (8 hex chars) so a UI-created collection\n * record looks like one the server would have generated for a form submitted\n * with a blank primary key. The source of randomness differs (UUID-derived here\n * vs `randomBytes` on the server); only the shape is intentionally shared.\n */\nexport function shortHexId(): string {\n return crypto.randomUUID().replace(/-/g, \"\").slice(0, 8);\n}\n","// Neutralize structural prompt-injection vectors in a short, record-derived\n// string before it rides into an LLM-facing prompt: strip angle brackets,\n// defang backticks / `${` template openings, collapse whitespace (so an\n// embedded newline can't fabricate a pseudo-instruction on its own line), and\n// clip to a small budget. Mirrors the host server's own defang so the two\n// can't drift (#1677).\n\n/** Max chars kept — the first batch of a validation issue is enough to act on. */\nconst DEFANG_MAX_LEN = 200;\n\nexport function defangForPrompt(value: string): string {\n return value.replace(/[<>]/g, \"\").replace(/`/g, \"'\").replace(/\\$\\{/g, \"$ {\").replace(/\\s+/g, \" \").slice(0, DEFANG_MAX_LEN);\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../../rolldown-runtime-D6vf50IK.cjs");
3
- const require_server = require("../../../server-Cd5WdM8a.cjs");
3
+ const require_server = require("../../../server-BeoOWQ-a.cjs");
4
4
  const require_collection_paths = require("../../paths.cjs");
5
5
  const require_types = require("../../../types-CNqkLT4M.cjs");
6
6
  const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
@@ -1,4 +1,4 @@
1
- import { B as writeFileAtomic, Q as log, S as CollectionSchemaZ, Y as collectionsRegistriesConfigPath, _ as acceptParsedSchema, m as errorMessage, p as ONE_SECOND_MS, q as safeRecordId, y as loadCollection } from "../../../server-Cc6XcyOA.js";
1
+ import { $ as collectionsRegistriesConfigPath, W as writeFileAtomic, Z as safeRecordId, m as errorMessage, nt as log, p as ONE_SECOND_MS, w as CollectionSchemaZ, x as loadCollection, y as acceptParsedSchema } from "../../../server-0IVR-Y_G.js";
2
2
  import { isSafeActionTemplatePath } from "../../paths.js";
3
3
  import { n as parseRegistryIndex, r as isRecord, t as OFFICIAL_REGISTRY_NAME } from "../../../types-BKVZrtyc.js";
4
4
  import { claudeSkillDir, dataSkillDir, mirrorSkillWrite } from "../../../skill-bridge/index.js";
@@ -1,13 +1,16 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_server = require("../../server-Cd5WdM8a.cjs");
2
+ const require_calendarGrid = require("../../calendarGrid-CRaMAguM.cjs");
3
+ const require_server = require("../../server-BeoOWQ-a.cjs");
3
4
  const require_collection_paths = require("../paths.cjs");
4
- exports.COMPUTED_TYPES = require_server.COMPUTED_TYPES;
5
+ exports.COMPUTED_TYPES = require_calendarGrid.COMPUTED_TYPES;
5
6
  exports.CollectionSchemaZ = require_server.CollectionSchemaZ;
6
7
  exports.SCHEMA_FILE = require_server.SCHEMA_FILE;
7
8
  exports.acceptParsedSchema = require_server.acceptParsedSchema;
8
9
  exports.advanceTriggerDate = require_server.advanceTriggerDate;
10
+ exports.applyMutateAction = require_server.applyMutateAction;
9
11
  exports.buildActionSeedPrompt = require_server.buildActionSeedPrompt;
10
12
  exports.buildCollectionActionSeedPrompt = require_server.buildCollectionActionSeedPrompt;
13
+ exports.buildWorkspaceOntology = require_server.buildWorkspaceOntology;
11
14
  exports.compileRecordZ = require_server.compileRecordZ;
12
15
  exports.computeCollectionIcon = require_server.computeCollectionIcon;
13
16
  exports.computeSuccessor = require_server.computeSuccessor;
@@ -19,11 +22,13 @@ exports.deleteCustomView = require_server.deleteCustomView;
19
22
  exports.deleteItem = require_server.deleteItem;
20
23
  exports.discoverCollections = require_server.discoverCollections;
21
24
  exports.enrichItems = require_server.enrichItems;
25
+ exports.firstMutateParamProblem = require_server.firstMutateParamProblem;
22
26
  exports.formatCivil = require_server.formatCivil;
23
27
  exports.generateItemId = require_server.generateItemId;
24
28
  exports.getWorkspaceRoot = require_server.getWorkspaceRoot;
25
29
  exports.isContainedInRoot = require_server.isContainedInRoot;
26
30
  exports.isContainedInWorkspace = require_server.isContainedInWorkspace;
31
+ exports.isRegularFile = require_server.isRegularFile;
27
32
  exports.isSafeActionTemplatePath = require_collection_paths.isSafeActionTemplatePath;
28
33
  exports.isSafeCustomViewI18nPath = require_collection_paths.isSafeCustomViewI18nPath;
29
34
  exports.isSafeCustomViewPath = require_collection_paths.isSafeCustomViewPath;
@@ -48,6 +53,7 @@ exports.resolveEvery = require_server.resolveEvery;
48
53
  exports.resolveTemplatePath = require_server.resolveTemplatePath;
49
54
  exports.safeRecordId = require_server.safeRecordId;
50
55
  exports.safeSlugName = require_server.safeSlugName;
56
+ exports.schemaRelations = require_server.schemaRelations;
51
57
  exports.setCollectionChangePublisher = require_server.setCollectionChangePublisher;
52
58
  exports.successorId = require_server.successorId;
53
59
  exports.toDetail = require_server.toDetail;
@@ -4,7 +4,9 @@ export * from './paths';
4
4
  export * from './templatePath';
5
5
  export * from './io';
6
6
  export * from './validate';
7
+ export * from './mutate';
7
8
  export * from './discovery';
9
+ export * from './ontology';
8
10
  export * from './derive';
9
11
  export * from './dynamicIcon';
10
12
  export * from './spawn';
@@ -1,3 +1,4 @@
1
- import { $ as publishCollectionChange, A as deleteItem, C as validateCollectionRecords, D as recordFieldProblem, E as compileRecordZ, F as readCustomViewI18n, G as resolveDataDir, H as isContainedInRoot, I as readItem, J as safeSlugName, K as resolveTemplatePath, L as readSkillTemplate, M as listItems, N as promptPathsFor, O as buildActionSeedPrompt, P as readCustomViewHtml, Q as log, R as resolveCreateItemId, S as CollectionSchemaZ, T as COMPUTED_TYPES, U as isContainedInWorkspace, V as SCHEMA_FILE, W as itemFilePath, X as configureCollectionHost, Z as getWorkspaceRoot, _ as acceptParsedSchema, a as computeSuccessor, b as toDetail, c as isTriggerDue, d as resolveEvery, et as setCollectionChangePublisher, f as successorId, g as enrichItems, h as computeCollectionIcon, i as advanceTriggerDate, j as generateItemId, k as buildCollectionActionSeedPrompt, l as maybeSpawnSuccessor, n as deleteCollection, o as daysInMonth, q as safeRecordId, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as discoverCollections, w as validateRecordObject, x as toSummary, y as loadCollection, z as writeItem } from "../../server-Cc6XcyOA.js";
1
+ import { F as COMPUTED_TYPES } from "../../calendarGrid-B3dtFN-m.js";
2
+ import { A as recordFieldProblem, B as readItem, C as toSummary, D as validateCollectionRecords, E as firstMutateParamProblem, F as isRegularFile, G as SCHEMA_FILE, H as resolveCreateItemId, I as listItems, J as itemFilePath, K as isContainedInRoot, L as promptPathsFor, M as buildCollectionActionSeedPrompt, N as deleteItem, O as validateRecordObject, P as generateItemId, Q as safeSlugName, R as readCustomViewHtml, S as toDetail, T as applyMutateAction, U as writeItem, V as readSkillTemplate, X as resolveTemplatePath, Y as resolveDataDir, Z as safeRecordId, _ as buildWorkspaceOntology, a as computeSuccessor, b as discoverCollections, c as isTriggerDue, d as resolveEvery, et as configureCollectionHost, f as successorId, g as enrichItems, h as computeCollectionIcon, i as advanceTriggerDate, it as setCollectionChangePublisher, j as buildActionSeedPrompt, k as compileRecordZ, l as maybeSpawnSuccessor, n as deleteCollection, nt as log, o as daysInMonth, q as isContainedInWorkspace, r as deleteCollectionRefusalMessage, rt as publishCollectionChange, s as formatCivil, t as deleteCustomView, tt as getWorkspaceRoot, u as parseCivil, v as schemaRelations, w as CollectionSchemaZ, x as loadCollection, y as acceptParsedSchema, z as readCustomViewI18n } from "../../server-0IVR-Y_G.js";
2
3
  import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
- export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
4
+ export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -14,6 +14,15 @@ export interface IoOptions {
14
14
  * omitting it just means no event is published (internal / test writes). */
15
15
  slug?: string;
16
16
  }
17
+ /** True iff `filePath` exists and is a regular file (NOT a symlink).
18
+ * Defends `listItems` / `readItem` against `*.json` symlinks placed
19
+ * inside an otherwise-contained data dir — without this, a record
20
+ * file could symlink to /etc/passwd and the detail endpoint would
21
+ * happily serve it. Returns false on ENOENT and on any other lstat
22
+ * failure so the caller's "missing" branch covers those cases too.
23
+ * Exported so `ontology.ts`'s record COUNT classifies entries with the
24
+ * SAME lstat logic — the two must agree on what a record file is. */
25
+ export declare function isRegularFile(filePath: string): Promise<boolean>;
17
26
  /** Read every record under `dataDir`. Returns [] if the dir doesn't
18
27
  * exist yet (legitimate first-use state). Malformed JSON files and
19
28
  * symlinked records are skipped (the latter is a file-disclosure
@@ -0,0 +1,24 @@
1
+ import { IoOptions } from './io';
2
+ import { LoadedCollection } from './discoveredCollection';
3
+ import { CollectionItem, CollectionMutateAction } from '../core/schema';
4
+ export type MutateActionOutcome = {
5
+ ok: true;
6
+ item: CollectionItem;
7
+ }
8
+ /** `status` picks the HTTP mapping: bad params / a write-gate reject are
9
+ * the caller's 400s, a missing record its 404, an unmet `require` its
10
+ * 409, a refused write its 500. */
11
+ | {
12
+ ok: false;
13
+ status: "invalid-params" | "invalid-record" | "not-found" | "require-unmet" | "write-refused";
14
+ problem: string;
15
+ };
16
+ /** First problem with the submitted params, or null. Every declared param
17
+ * is checked by the shared record-field validator; keys the action never
18
+ * declared are rejected outright — a stray key would otherwise ride the
19
+ * resolved `set` semantics silently. */
20
+ export declare function firstMutateParamProblem(action: CollectionMutateAction, params: Record<string, unknown>): string | null;
21
+ /** Apply one mutate action to one record. Never throws for data-shaped
22
+ * failures — the outcome's `problem` doubles as agent feedback when an
23
+ * LLM (not the user) pressed the button. */
24
+ export declare function applyMutateAction(collection: LoadedCollection, action: CollectionMutateAction, itemId: string, params: Record<string, unknown>, opts?: IoOptions): Promise<MutateActionOutcome>;
@@ -0,0 +1,35 @@
1
+ import { DiscoveryOptions } from './discovery';
2
+ import { CollectionSchema } from '../core/schema';
3
+ /** One relationship a schema declares: a `ref` (the record stores the
4
+ * target's primaryKey slug) or `embed` (display-only pull) pointing at
5
+ * collection `to`, or `backlinks` (display-only REVERSE refs — `to` is
6
+ * the backlink's source collection, i.e. its `from`). A `ref` column
7
+ * inside a `table` field is reported with a dotted path
8
+ * (`lines.clientId`). Whether `to` exists is NOT checked — resolution
9
+ * is fail-soft at render, and the caller holds the full slug list to
10
+ * compare against anyway. */
11
+ export interface OntologyRelation {
12
+ field: string;
13
+ kind: "ref" | "embed" | "backlinks" | "rollup";
14
+ to: string;
15
+ }
16
+ export interface CollectionOntologyEntry {
17
+ slug: string;
18
+ title: string;
19
+ icon: string;
20
+ primaryKey: string;
21
+ /** The effective display field: the schema's `displayField`, falling
22
+ * back to the primaryKey exactly as render-time labelling does. */
23
+ displayField: string;
24
+ recordCount: number;
25
+ relations: OntologyRelation[];
26
+ }
27
+ /** Extract the relations a schema declares, in field declaration order:
28
+ * top-level `ref` / `embed` / `backlinks` fields plus `ref` sub-fields
29
+ * inside `table` columns. Pure — exported so the phase-2 graph panel
30
+ * can reuse it on already-loaded schemas. */
31
+ export declare function schemaRelations(schema: CollectionSchema): OntologyRelation[];
32
+ /** Build the workspace ontology: every discovered collection (slug-sorted,
33
+ * discovery's order), each with its outbound relations and a
34
+ * readdir-cheap record count — fine to call on demand. */
35
+ export declare function buildWorkspaceOntology(opts?: DiscoveryOptions): Promise<CollectionOntologyEntry[]>;
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_collection_index = require("../collection/index.cjs");
3
- const require_server = require("../server-Cd5WdM8a.cjs");
2
+ const require_calendarGrid = require("../calendarGrid-CRaMAguM.cjs");
3
+ require("../collection/index.cjs");
4
+ const require_server = require("../server-BeoOWQ-a.cjs");
4
5
  const require_notifier = require("../notifier-bS8IEeLA.cjs");
5
6
  let node_fs = require("node:fs");
6
7
  let node_fs_promises = require("node:fs/promises");
@@ -224,7 +225,7 @@ async function reconcileItem(slug, schema, dataDir, itemId, ioOpts = {}, now = e
224
225
  return;
225
226
  }
226
227
  }
227
- if (!require_collection_index.whenMatches(schema.notifyWhen, item)) {
228
+ if (!require_calendarGrid.whenMatches(schema.notifyWhen, item)) {
228
229
  await clearItemNotification(slug, itemId);
229
230
  return;
230
231
  }
@@ -280,7 +281,7 @@ async function sweepStaleActiveEntries(opts = {}) {
280
281
  continue;
281
282
  }
282
283
  const item = await require_server.readItem(collection.dataDir, itemId, opts);
283
- if (item === null || itemIsDone(collection.schema, item) || !require_collection_index.whenMatches(collection.schema.notifyWhen, item)) await require_notifier.clear(entry.id);
284
+ if (item === null || itemIsDone(collection.schema, item) || !require_calendarGrid.whenMatches(collection.schema.notifyWhen, item)) await require_notifier.clear(entry.id);
284
285
  } catch (err) {
285
286
  log().warn("sweep entry failed", {
286
287
  slug,
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/collection-watchers/config.ts","../../src/collection-watchers/clock.ts","../../src/collection-watchers/reconciler.ts","../../src/collection-watchers/watcher.ts"],"sourcesContent":["// Host-injected configuration for the collection-completion watchers.\n// The reconciler logic + watcher plumbing are host-agnostic; the\n// notification TAXONOMY (which plugin namespace, how a record priority\n// maps to a bell severity) and the in-app ROUTING (the deep-link a bell\n// row navigates to) are host-specific, so the host supplies them via an\n// adapter. MulmoClaude wires its legacy notification machinery; a future\n// MulmoTerminal wires its own routes + pluginData shape.\n\nimport type { NotifierSeverity } from \"../notifier\";\n\n/** Two-level urgency a pending record can carry, derived from the\n * schema's `notifyWhen` spec. The host maps this onto its own severity\n * scale via `priorityToSeverity`. */\nexport type CompletionPriority = \"normal\" | \"high\";\n\nexport interface CollectionWatcherLogger {\n info: (message: string, data?: Record<string, unknown>) => void;\n warn: (message: string, data?: Record<string, unknown>) => void;\n}\n\n/** The host-specific notification surface the reconciler binds to. The\n * reconciler owns the internal `legacyId` key (it encodes slug+itemId\n * and round-trips it through `pluginData`); the adapter only wraps /\n * unwraps it into whatever shape the host's bell expects. */\nexport interface CollectionNotificationAdapter {\n /** Plugin namespace these bell entries publish under (MulmoClaude: \"todo\"). */\n pluginPkg: string;\n /** Map a record's completion priority onto the host's bell severity. */\n priorityToSeverity: (priority: CompletionPriority) => NotifierSeverity;\n /** Build the in-app deep-link the bell row routes to on click. */\n buildNavigateTarget: (slug: string, itemId: string) => string;\n /** Wrap the reconciler's internal key + priority into the host's\n * `pluginData` shape. Stored verbatim on the entry; recovered via\n * `readEntry`. */\n buildPluginData: (input: { legacyId: string; slug: string; itemId: string; priority: CompletionPriority; navigateTarget: string }) => unknown;\n /** Recognise a bell entry produced by this reconciler and recover its\n * internal key + stored priority. Returns null for entries that didn't\n * originate here, so `listAll()` scans skip foreign entries. */\n readEntry: (pluginData: unknown) => { legacyId: string; priority: CompletionPriority } | null;\n}\n\nconst NOOP_LOG: CollectionWatcherLogger = { info: () => {}, warn: () => {} };\n\nlet adapter: CollectionNotificationAdapter | null = null;\nlet activeLogger: CollectionWatcherLogger = NOOP_LOG;\n\n/** Wire the host adapter + logger. Call once at startup, before\n * `startCollectionWatchers` or any direct reconcile call. */\nexport function configureCollectionWatchers(config: { adapter: CollectionNotificationAdapter; log?: CollectionWatcherLogger }): void {\n ({ adapter } = config);\n activeLogger = config.log ?? NOOP_LOG;\n}\n\nexport function requireAdapter(): CollectionNotificationAdapter {\n if (!adapter) throw new Error(\"collection-watchers: configureCollectionWatchers() not called\");\n return adapter;\n}\n\nexport function log(): CollectionWatcherLogger {\n return activeLogger;\n}\n\n/** Test-only: clear the host wiring. */\nexport function resetCollectionWatchersConfig(): void {\n adapter = null;\n activeLogger = NOOP_LOG;\n}\n\nexport function errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","// Evaluation-only injectable clock. When MULMOCLAUDE_FAKE_NOW is set to\n// a parseable date/datetime string, every reconcile pass derives \"now\"\n// from it instead of the wall clock — letting evaluation runs and tests\n// advance time deterministically (a `triggerField` coming due, a `spawn`\n// successor's own trigger firing later) without waiting for real days to\n// pass. Unset or unparseable (i.e. normal operation) → real wall clock.\n// Read per call, so a long-lived server picks up changes without restart.\nexport function evalNow(): Date {\n const raw = process.env.MULMOCLAUDE_FAKE_NOW;\n if (!raw) return new Date();\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? new Date() : parsed;\n}\n","// Bell-notification reconciler for collections whose schema declares\n// `completionField`. Driven by `watcher.ts`, which calls into the\n// functions below on file-system events and on boot.\n//\n// The model is **convergent**: a watcher event re-reads the record from\n// disk and the reconciler enforces the invariant\n//\n// bell entry exists for (slug, itemId) ↔\n// schema has completionField ∧\n// file exists ∧\n// `String(item[completionField])` ∉ completionDoneValues\n// (∧ trigger due ∧ notifyWhen matches, when declared)\n//\n// Each reconcile is idempotent (`ensure*` / `clear*` no-op when state\n// already matches). This is why event-type quirks of `fs.watch`\n// (`rename` vs `change`, missed events, atomic-write coalescence) don't\n// matter — every event re-derives the desired state from the file.\n//\n// Lookup uses a deterministic internal `legacyId` derived from\n// `<slug>:<itemId>`, stashed on each entry's `pluginData` via the host\n// adapter, so the clear path and the dedup check both find the entry\n// without a side state file.\n\nimport { clear as notifierClear, listAll, publish as notifierPublish, updateForPlugin as notifierUpdate, type NotifierEntry } from \"../notifier\";\nimport { whenMatches, type CollectionItem, type CollectionSchema } from \"../collection\";\nimport { type DiscoveryOptions, listItems, readItem, type IoOptions, isTriggerDue, maybeSpawnSuccessor, loadCollection } from \"../collection/server\";\nimport { type CompletionPriority, errMsg, log, requireAdapter } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\n\n/** The internal-id prefix every collection-completion bell entry carries.\n * Used both to build new keys and to filter sweep candidates from the\n * active bell. */\nconst LEGACY_ID_PREFIX = \"collection-completion:\";\n\n/** Stable key encoding slug + item, round-tripped through the entry's\n * `pluginData` so we can find it later without a side state file. Slug +\n * itemId are upstream-validated via `safeSlugName`, which forbids the\n * colon separator, so the two-segment parse below is unambiguous. */\nfunction completionLegacyId(slug: string, itemId: string): string {\n return `${LEGACY_ID_PREFIX}${slug}:${itemId}`;\n}\n\n/** Decode a key back into its (slug, itemId) pair, or null if the string\n * didn't originate from this module. Used by the sweep step. */\nfunction parseCompletionLegacyId(legacyId: string): { slug: string; itemId: string } | null {\n if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;\n const body = legacyId.slice(LEGACY_ID_PREFIX.length);\n const colon = body.indexOf(\":\");\n if (colon < 0) return null;\n return { slug: body.slice(0, colon), itemId: body.slice(colon + 1) };\n}\n\n/** The human-readable label shown in a completion notification's title.\n * Uses the schema's `displayField` value when declared and non-empty;\n * otherwise falls back to the record's primaryKey (`itemId`). */\nexport function resolveDisplayLabel(schema: CollectionSchema, item: CollectionItem, itemId: string): string {\n const { displayField } = schema;\n if (!displayField) return itemId;\n const raw = item[displayField];\n if (raw === undefined || raw === null) return itemId;\n const label = String(raw).trim();\n return label.length > 0 ? label : itemId;\n}\n\n/** True iff the schema declares completion tracking AND the item's\n * `completionField` value (stringified) is in `completionDoneValues`. */\nexport function itemIsDone(schema: CollectionSchema, item: CollectionItem): boolean {\n const { completionField, completionDoneValues } = schema;\n if (!completionField || !completionDoneValues) return false;\n const raw = item[completionField];\n if (raw === undefined || raw === null) return false;\n return completionDoneValues.includes(String(raw));\n}\n\n/** Every active bell entry whose key matches this (slug, itemId).\n * Returns multiple when defensive cleanup is needed. Scans `listAll()`\n * — cheap because the active set is bounded. */\nasync function findActiveEntries(slug: string, itemId: string): Promise<NotifierEntry[]> {\n const adapter = requireAdapter();\n const legacyId = completionLegacyId(slug, itemId);\n const entries = await listAll();\n return entries.filter((entry) => adapter.readEntry(entry.pluginData)?.legacyId === legacyId);\n}\n\nasync function findActiveEntryIds(slug: string, itemId: string): Promise<string[]> {\n return (await findActiveEntries(slug, itemId)).map((entry) => entry.id);\n}\n\n/** Per-key in-flight lock. Serializes concurrent `ensureItemNotification`\n * calls for the same (slug, itemId) so the `findActiveEntries → publish`\n * check stays atomic across callers — not just across watcher events.\n * `listAll` bypasses the engine's write queue, so without this lock two\n * reconcile paths (a watcher event + a `reconcileAllItems` pass that a\n * readdir-triggered event raced) could both miss each other's in-flight\n * publish and produce duplicate entries. */\ninterface EnsureLock {\n promise: Promise<void>;\n}\nconst ensureLocks = new Map<string, EnsureLock>();\n\n/** Bell priority for a record: the FIRST flagged value in `notifyWhen.in`\n * (most urgent) reads `high`, every other flagged value `normal`.\n * Collections with no `notifyWhen` (notify for every open record) stay\n * `normal`. */\nfunction notifyPriorityForItem(schema: CollectionSchema, item: CollectionItem): CompletionPriority {\n const spec = schema.notifyWhen;\n if (!spec) return \"normal\";\n const value = item[spec.field] === undefined || item[spec.field] === null ? \"\" : String(item[spec.field]);\n return spec.in.indexOf(value) === 0 ? \"high\" : \"normal\";\n}\n\nasync function ensureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const legacyId = completionLegacyId(slug, itemId);\n // Drain any in-flight publish for this key BEFORE our check + set. The\n // drain + claim runs synchronously between `ensureLocks.get` and\n // `ensureLocks.set`, so two callers can't both observe an empty slot.\n\n while (true) {\n const inflight = ensureLocks.get(legacyId);\n if (!inflight) break;\n await inflight.promise;\n }\n const lock: EnsureLock = { promise: doEnsureItemNotification(slug, schema, itemId, legacyId, displayLabel, priority) };\n ensureLocks.set(legacyId, lock);\n try {\n await lock.promise;\n } finally {\n // Only clear the slot if it still points at OUR lock — a\n // sufficiently-delayed cleanup must not stomp a later claim.\n if (ensureLocks.get(legacyId) === lock) {\n ensureLocks.delete(legacyId);\n }\n }\n}\n\n/** Converge any already-present bell entries to `priority`, updating in\n * place (preserving id / position / createdAt) so a record whose flagged\n * value changed while it stayed pending re-colours the bell without a\n * clear+republish flicker. No-op when the stored priority already matches. */\nasync function reconcileEntrySeverity(slug: string, itemId: string, entries: NotifierEntry[], priority: CompletionPriority): Promise<void> {\n const adapter = requireAdapter();\n for (const entry of entries) {\n const parsed = adapter.readEntry(entry.pluginData);\n if (!parsed || parsed.priority === priority) continue;\n await notifierUpdate(adapter.pluginPkg, entry.id, {\n severity: adapter.priorityToSeverity(priority),\n pluginData: adapter.buildPluginData({\n legacyId: parsed.legacyId,\n slug,\n itemId,\n priority,\n navigateTarget: adapter.buildNavigateTarget(slug, itemId),\n }),\n });\n }\n}\n\nasync function doEnsureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n legacyId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const adapter = requireAdapter();\n try {\n const existing = await findActiveEntries(slug, itemId);\n if (existing.length > 0) {\n await reconcileEntrySeverity(slug, itemId, existing, priority);\n return;\n }\n const navigateTarget = adapter.buildNavigateTarget(slug, itemId);\n // `lifecycle: \"action\"` — these are state-of-the-world entries\n // mirroring an outstanding obligation (the item is pending), not\n // transient pings. Validation requires a non-info severity and a\n // non-empty `navigateTarget` (the slug + itemId deep-link).\n await notifierPublish({\n pluginPkg: adapter.pluginPkg,\n severity: adapter.priorityToSeverity(priority),\n lifecycle: \"action\",\n title: `${schema.title}: ${displayLabel}`,\n navigateTarget,\n pluginData: adapter.buildPluginData({ legacyId, slug, itemId, priority, navigateTarget }),\n });\n } catch (err) {\n log().warn(\"notify ensure failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Idempotently clear EVERY bell entry that matches this (slug, itemId).\n * Silent no-op when nothing matches. The \"every\" is defensive: if a\n * duplicate ever slips through, this drains the lot. */\nexport async function clearItemNotification(slug: string, itemId: string): Promise<void> {\n try {\n const ids = await findActiveEntryIds(slug, itemId);\n for (const entryId of ids) {\n await notifierClear(entryId);\n }\n } catch (err) {\n log().warn(\"notify clear failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Reconcile one item to the desired bell state. Re-reads the record from\n * disk so the decision is grounded in current truth, not in the event\n * payload. Safe to call when the file is missing (delete path).\n *\n * `ioOpts` flows into `readItem`'s workspace-containment check —\n * production callers (the watcher) pass nothing; tests pass\n * `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */\nexport async function reconcileItem(\n slug: string,\n schema: CollectionSchema,\n dataDir: string,\n itemId: string,\n ioOpts: IoOptions = {},\n now: Date = evalNow(),\n): Promise<void> {\n if (!schema.completionField) {\n // Schema doesn't track completion — drop any stale entry.\n await clearItemNotification(slug, itemId);\n return;\n }\n const item = await readItem(dataDir, itemId, ioOpts);\n if (item === null) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Recurrence: predicate-gated + create-if-absent, idempotent and\n // independent of this item's own bell state. Runs before the done-clear\n // below so marking an item done still spawns its successor.\n await maybeSpawnSuccessor(slug, schema, dataDir, item, itemId, ioOpts);\n if (itemIsDone(schema, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Time gate: when the schema declares `triggerField`, suppress the bell\n // until the clock reaches that date (minus `triggerLeadDays`).\n // Unparseable date ⇒ fail safe (no bell); warn ONLY when the field carries\n // a non-empty value that won't parse — an empty optional trigger date is a\n // normal state and must not spam a WARN every reconcile tick.\n if (schema.triggerField) {\n const triggerRaw = item[schema.triggerField];\n const due = isTriggerDue(triggerRaw, now, schema.triggerLeadDays);\n const isEmpty = triggerRaw === undefined || triggerRaw === null || triggerRaw === \"\";\n if (due === null && !isEmpty) {\n log().warn(\"trigger date unparseable, suppressing bell\", { slug, itemId, triggerField: schema.triggerField });\n }\n if (due !== true) {\n await clearItemNotification(slug, itemId);\n return;\n }\n }\n // Condition gate: when the schema declares `notifyWhen`, only bell\n // records matching the predicate. Convergent — a record that stops\n // matching has its bell cleared.\n if (!whenMatches(schema.notifyWhen, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));\n}\n\n/** Boot-time reconcile: walk every record under `dataDir` once and\n * reconcile it. Catches up changes that happened while the server was\n * down. Deleted items are covered by `sweepStaleActiveEntries`, not this\n * function (it only sees files that exist). */\nexport async function reconcileAllItems(slug: string, schema: CollectionSchema, dataDir: string, ioOpts: IoOptions = {}, now: Date = evalNow()): Promise<void> {\n if (!schema.completionField) return;\n let items: CollectionItem[];\n try {\n items = await listItems(dataDir, ioOpts);\n } catch (err) {\n log().warn(\"reconcile list failed\", { slug, dataDir, error: errMsg(err) });\n return;\n }\n const { primaryKey } = schema;\n for (const item of items) {\n const raw = item[primaryKey];\n if (typeof raw !== \"string\" || raw.length === 0) continue;\n await reconcileItem(slug, schema, dataDir, raw, ioOpts, now);\n }\n}\n\n/** Boot-time sweep over the active bell: drop any entries whose underlying\n * file is gone, whose collection was deleted, whose schema no longer\n * tracks completion, or whose item is now done. Reverse-covers the cases\n * `reconcileAllItems` misses (it only walks files that exist). */\nexport async function sweepStaleActiveEntries(opts: DiscoveryOptions = {}): Promise<void> {\n const adapter = requireAdapter();\n let entries;\n try {\n entries = await listAll();\n } catch (err) {\n log().warn(\"sweep list failed\", { error: errMsg(err) });\n return;\n }\n for (const entry of entries) {\n const own = adapter.readEntry(entry.pluginData);\n if (!own) continue;\n const parsed = parseCompletionLegacyId(own.legacyId);\n if (!parsed) continue;\n const { slug, itemId } = parsed;\n try {\n const collection = await loadCollection(slug, opts);\n if (!collection || !collection.schema.completionField) {\n await notifierClear(entry.id);\n continue;\n }\n const item = await readItem(collection.dataDir, itemId, opts);\n if (item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item)) {\n await notifierClear(entry.id);\n }\n } catch (err) {\n log().warn(\"sweep entry failed\", { slug, itemId, error: errMsg(err) });\n }\n }\n}\n\n/** Test-only: clear the per-key in-flight locks. */\nexport function _resetReconcilerLocksForTesting(): void {\n ensureLocks.clear();\n}\n","// Filesystem watchers that drive collection-completion bell\n// notifications. One `fs.watch` per discovered collection's `dataDir`,\n// fanned out from a single boot call + a 30-second re-discovery interval\n// that catches newly-created / deleted collections (there is no\n// in-process \"collections changed\" event broadcast).\n//\n// Why a watcher, not just route hooks: the canonical pattern for\n// collection-skills has the agent Write records directly with the Write\n// tool — that path never hits the REST API, so a route-level hook would\n// miss most of the traffic the user generates. The watcher catches every\n// mutation regardless of who wrote the file.\n//\n// All decisions live in `reconciler.ts`; this module is pure plumbing:\n// discover, mkdir, fs.watch, forward events into the reconciler. Every\n// reconcile call is idempotent so fs.watch's well-known quirks (`rename`\n// vs `change`, atomic-write coalescence, filename === null on some\n// platforms) don't need special handling.\n\nimport { watch, type FSWatcher } from \"node:fs\";\nimport { mkdir } from \"node:fs/promises\";\nimport { discoverCollections, loadCollection, type DiscoveryOptions, type LoadedCollection } from \"../collection/server\";\nimport type { CollectionSchema } from \"../collection\";\nimport { errMsg, log } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\nimport { reconcileAllItems, reconcileItem, sweepStaleActiveEntries } from \"./reconciler.js\";\n\n// Collections don't get added / removed rapidly; 30 s is a comfortable\n// upper bound on how long a new schema can sit before its watcher is up.\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\nconst REDISCOVERY_INTERVAL_MS = 30 * ONE_SECOND_MS;\n\n// Wall-clock tick that re-reconciles time-dependent collections (those\n// declaring `triggerField` and/or `spawn`). The fs.watcher only re-runs\n// the reconciler on FILE changes; a `triggerField` bell that should fire\n// \"when the clock reaches date X\" — and a `spawn` whose successor's own\n// trigger later comes due — change no file at that moment, so a periodic\n// re-derivation is required.\nconst TRIGGER_TICK_INTERVAL_MS = ONE_MINUTE_MS;\n\ninterface CollectionWatcher {\n slug: string;\n dataDir: string;\n watcher: FSWatcher;\n /** Last-seen serialized schema for change detection. When a rediscovery\n * tick observes a different value, the watcher's items are reconciled\n * and the cache is refreshed — this catches schema-only edits (e.g.\n * flipping `completionField` on or off) that don't touch any record\n * file and would otherwise leave bell state stale indefinitely. */\n schemaJson: string;\n}\n\nconst watchers = new Map<string, CollectionWatcher>();\nlet rediscoveryTimer: ReturnType<typeof setInterval> | null = null;\nlet triggerTimer: ReturnType<typeof setInterval> | null = null;\nlet started = false;\n/** Discovery options threaded into every `discoverCollections` /\n * `loadCollection` / `sweepStaleActiveEntries` call. Production: empty\n * (live workspace). Tests: `{ workspaceRoot, userSkillsDir }` pointing\n * at a fixture tree. Module-level so per-event handlers can read it\n * without threading through every signature. */\nlet discoveryOpts: DiscoveryOptions = {};\n\n/** Per-key single-flight slot (declared here so `stopCollectionWatchers`\n * can clear it during teardown). */\ninterface ReconcileSlot {\n running: Promise<void>;\n pending: boolean;\n}\nconst itemSlots = new Map<string, ReconcileSlot>();\n\n/** Test-only configuration knobs. Production callers pass nothing and get\n * the live workspace defaults; tests pass a tmpdir-rooted `discoveryOpts`\n * and override the tick cadences (or set them to `null` to disable the\n * auto-ticks so the test drives sync manually). */\nexport interface CollectionWatcherOptions {\n discoveryOpts?: DiscoveryOptions;\n rediscoveryIntervalMs?: number | null;\n triggerTickIntervalMs?: number | null;\n}\n\n/** Boot entry point: sweep stale active entries, then mount watchers for\n * every discovered collection and arm the periodic re-discovery poll.\n * Idempotent — a second call is a no-op. */\nexport async function startCollectionWatchers(opts: CollectionWatcherOptions = {}): Promise<void> {\n if (started) return;\n // `started` only flips on AFTER boot finishes. If sweep or syncWatchers\n // throws mid-boot, reset state on failure so a supervisor / test\n // harness can retry instead of being permanently latched.\n discoveryOpts = opts.discoveryOpts ?? {};\n try {\n // Boot reconcile is split in two: sweep first (drop bell entries whose\n // files / collections / schemas vanished while the server was down),\n // then `syncWatchers` runs the per-collection forward fill. Both paths\n // are idempotent and converge on the same end state.\n await sweepStaleActiveEntries(discoveryOpts);\n await syncWatchers();\n const intervalMs = opts.rediscoveryIntervalMs === undefined ? REDISCOVERY_INTERVAL_MS : opts.rediscoveryIntervalMs;\n if (intervalMs !== null) {\n rediscoveryTimer = setInterval(() => {\n syncWatchers().catch((err: unknown) => {\n log().warn(\"watcher rediscovery failed\", { error: errMsg(err) });\n });\n }, intervalMs);\n // `unref` so a clean process exit isn't blocked waiting for the tick.\n rediscoveryTimer.unref();\n }\n const triggerMs = opts.triggerTickIntervalMs === undefined ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;\n if (triggerMs !== null) {\n triggerTimer = setInterval(() => {\n tickTimeTriggers().catch((err: unknown) => {\n log().warn(\"watcher trigger tick failed\", { error: errMsg(err) });\n });\n }, triggerMs);\n triggerTimer.unref();\n }\n started = true;\n } catch (err) {\n discoveryOpts = {};\n throw err;\n }\n}\n\n/** Tear down every watcher and stop the intervals. Used by tests;\n * production never calls this (process exit reclaims the fds). Resets\n * `started` so a subsequent `startCollectionWatchers` re-mounts. */\nexport async function stopCollectionWatchers(): Promise<void> {\n if (rediscoveryTimer) {\n clearInterval(rediscoveryTimer);\n rediscoveryTimer = null;\n }\n if (triggerTimer) {\n clearInterval(triggerTimer);\n triggerTimer = null;\n }\n for (const watcher of watchers.values()) {\n try {\n watcher.watcher.close();\n } catch {\n /* fs.watch close is best-effort */\n }\n }\n watchers.clear();\n itemSlots.clear();\n discoveryOpts = {};\n started = false;\n}\n\n/** Test-only: manually trigger one rediscovery + reconcile pass. */\nexport async function _syncWatchersForTesting(): Promise<void> {\n await syncWatchers();\n}\n\n/** Test-only: drive one wall-clock tick synchronously, with an optional\n * injected clock. */\nexport async function _tickTimeTriggersForTesting(now?: Date): Promise<void> {\n await tickTimeTriggers(now);\n}\n\n/** Re-reconcile every watched collection that depends on the clock — i.e.\n * declares `triggerField` (a bell that fires at a date) and/or `spawn`\n * (recurrence whose successors come due over time). Collections with\n * neither are skipped. Idempotent. The schema is parsed back from the\n * watcher's cached `schemaJson` to avoid a per-tick disk read. */\nasync function tickTimeTriggers(now: Date = evalNow()): Promise<void> {\n for (const entry of watchers.values()) {\n let schema: CollectionSchema;\n try {\n schema = JSON.parse(entry.schemaJson) as CollectionSchema;\n } catch (err) {\n log().warn(\"trigger tick: bad cached schema\", { slug: entry.slug, error: errMsg(err) });\n continue;\n }\n if (!schema.triggerField && !schema.spawn) continue;\n await reconcileAllItems(entry.slug, schema, entry.dataDir, discoveryOpts, now);\n }\n}\n\n/** Reconcile the watcher set against the currently-discovered\n * collections. Adds watchers for new slugs (with a boot reconcile of\n * their items), drops watchers for vanished slugs, and re-reconciles\n * items for collections whose schema changed. Runs a final sweep when\n * this tick changed the watcher set or any schema. */\nasync function syncWatchers(): Promise<void> {\n let collections;\n try {\n collections = await discoverCollections(discoveryOpts);\n } catch (err) {\n log().warn(\"watcher discover failed\", { error: errMsg(err) });\n return;\n }\n const liveSlugs = new Set(collections.map((collection) => collection.slug));\n const vanishedMutated = stopVanishedWatchers(liveSlugs);\n const schemaMutated = await reconcileChangedSchemas(collections);\n const addedMutated = await startNewWatchers(collections);\n if (vanishedMutated || schemaMutated || addedMutated) {\n await sweepStaleActiveEntries(discoveryOpts);\n }\n}\n\nfunction stopVanishedWatchers(liveSlugs: Set<string>): boolean {\n let mutated = false;\n for (const slug of [...watchers.keys()]) {\n if (liveSlugs.has(slug)) continue;\n const watcher = watchers.get(slug);\n if (watcher) {\n try {\n watcher.watcher.close();\n } catch {\n /* best-effort */\n }\n }\n watchers.delete(slug);\n mutated = true;\n log().info(\"watcher stopped\", { slug });\n }\n return mutated;\n}\n\n/** Re-reconcile already-watched collections whose schema changed since\n * the last tick. New collections fall through to `startNewWatchers`. */\nasync function reconcileChangedSchemas(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n const existing = watchers.get(collection.slug);\n if (!existing) continue;\n const nextJson = JSON.stringify(collection.schema);\n if (existing.schemaJson === nextJson) continue;\n existing.schemaJson = nextJson;\n log().info(\"watcher schema changed, re-reconciling\", { slug: collection.slug });\n await reconcileAllItems(collection.slug, collection.schema, collection.dataDir, discoveryOpts);\n mutated = true;\n }\n return mutated;\n}\n\nasync function startNewWatchers(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n if (watchers.has(collection.slug)) continue;\n await startWatcherFor(collection.slug, collection.schema, collection.dataDir);\n mutated = true;\n }\n return mutated;\n}\n\nasync function startWatcherFor(slug: string, schema: CollectionSchema, dataDir: string): Promise<void> {\n try {\n // `fs.watch` throws on a missing dir, so ensure it exists. New\n // collections legitimately start with no records — mkdir is the\n // canonical first-use bootstrap.\n await mkdir(dataDir, { recursive: true });\n // Boot reconcile this collection's existing items BEFORE mounting the\n // watcher: a pending item the user added during downtime needs its\n // bell entry even if no event fires today.\n await reconcileAllItems(slug, schema, dataDir, discoveryOpts);\n const watcher = watch(dataDir, { persistent: false }, (_eventType, filename) => {\n // Errors from inside the callback would propagate as unhandled\n // rejections — wrap so a single bad event can't unwind the watcher.\n onEvent(slug, filename).catch((err: unknown) => {\n log().warn(\"watcher event failed\", { slug, filename, error: errMsg(err) });\n });\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"watcher error\", { slug, error: errMsg(err) });\n });\n watchers.set(slug, { slug, dataDir, watcher, schemaJson: JSON.stringify(schema) });\n log().info(\"watcher started\", { slug, dataDir });\n } catch (err) {\n log().warn(\"watcher start failed\", { slug, error: errMsg(err) });\n }\n}\n\n/** Test-only: the per-key single-flight scheduler. Exported so test code\n * can drive rapid-fire calls directly and observe the trailing coalesce\n * — `fs.watch` event timing is too flaky to assert against.\n *\n * Single-flight semantics: while a reconcile is in flight for a given\n * (slug, itemId), additional events on the same key set `pending = true`\n * and return — the running reconcile re-runs once after it completes.\n * This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as\n * 2-3 events) into a single reconcile + one trailing re-run. */\nexport function _scheduleItemReconcileForTesting(slug: string, schema: CollectionSchema, dataDir: string, itemId: string): Promise<void> {\n return scheduleItemReconcile(slug, schema, dataDir, itemId);\n}\n\nfunction scheduleItemReconcile(slug: string, schema: CollectionSchema, dataDir: string, itemId: string): Promise<void> {\n const key = `${slug}\\x00${itemId}`;\n const existing = itemSlots.get(key);\n if (existing) {\n existing.pending = true;\n return existing.running;\n }\n const slot: ReconcileSlot = { running: Promise.resolve(), pending: false };\n slot.running = (async () => {\n try {\n // Re-run while events keep arriving — the trailing re-run captures\n // any state change that landed during a prior pass. After each pass\n // we read `pending` and zero it before the next iteration, so an\n // event that fires *during* the last reconcile's await still\n // triggers one more pass before the slot is freed.\n let keepGoing = true;\n while (keepGoing) {\n slot.pending = false;\n await reconcileItem(slug, schema, dataDir, itemId, discoveryOpts);\n keepGoing = slot.pending;\n }\n } finally {\n itemSlots.delete(key);\n }\n })();\n itemSlots.set(key, slot);\n return slot.running;\n}\n\n/** Handle a single fs.watch event. Re-loads the collection (schema may\n * have changed since startup), filters out non-record files, and\n * forwards to the single-flighted reconciler. `filename === null` (rare,\n * platform-specific) triggers a full directory rescan to be safe. */\nasync function onEvent(slug: string, filename: string | Buffer | null): Promise<void> {\n const collection = await loadCollection(slug, discoveryOpts);\n if (!collection) return;\n if (filename === null) {\n // Some platforms omit the filename on a watch event — we don't know\n // which record changed. `reconcileAllItems` covers items whose file\n // still exists; pair it with a sweep so any record deleted inside the\n // same opaque event has its stale bell entry cleared too.\n await reconcileAllItems(slug, collection.schema, collection.dataDir, discoveryOpts);\n await sweepStaleActiveEntries(discoveryOpts);\n return;\n }\n const name = typeof filename === \"string\" ? filename : filename.toString(\"utf-8\");\n // Filter: only record files (`*.json`), skip dot-prefixed (atomic\n // writes / OS metadata / editor swap files). The reconciler is\n // idempotent so a stray non-record event would be harmless, but\n // skipping early avoids needless I/O.\n if (!name.endsWith(\".json\") || name.startsWith(\".\")) return;\n const itemId = name.slice(0, -\".json\".length);\n await scheduleItemReconcile(slug, collection.schema, collection.dataDir, itemId);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/collection-watchers/config.ts","../../src/collection-watchers/clock.ts","../../src/collection-watchers/reconciler.ts","../../src/collection-watchers/watcher.ts"],"sourcesContent":["// Host-injected configuration for the collection-completion watchers.\n// The reconciler logic + watcher plumbing are host-agnostic; the\n// notification TAXONOMY (which plugin namespace, how a record priority\n// maps to a bell severity) and the in-app ROUTING (the deep-link a bell\n// row navigates to) are host-specific, so the host supplies them via an\n// adapter. MulmoClaude wires its legacy notification machinery; a future\n// MulmoTerminal wires its own routes + pluginData shape.\n\nimport type { NotifierSeverity } from \"../notifier\";\n\n/** Two-level urgency a pending record can carry, derived from the\n * schema's `notifyWhen` spec. The host maps this onto its own severity\n * scale via `priorityToSeverity`. */\nexport type CompletionPriority = \"normal\" | \"high\";\n\nexport interface CollectionWatcherLogger {\n info: (message: string, data?: Record<string, unknown>) => void;\n warn: (message: string, data?: Record<string, unknown>) => void;\n}\n\n/** The host-specific notification surface the reconciler binds to. The\n * reconciler owns the internal `legacyId` key (it encodes slug+itemId\n * and round-trips it through `pluginData`); the adapter only wraps /\n * unwraps it into whatever shape the host's bell expects. */\nexport interface CollectionNotificationAdapter {\n /** Plugin namespace these bell entries publish under (MulmoClaude: \"todo\"). */\n pluginPkg: string;\n /** Map a record's completion priority onto the host's bell severity. */\n priorityToSeverity: (priority: CompletionPriority) => NotifierSeverity;\n /** Build the in-app deep-link the bell row routes to on click. */\n buildNavigateTarget: (slug: string, itemId: string) => string;\n /** Wrap the reconciler's internal key + priority into the host's\n * `pluginData` shape. Stored verbatim on the entry; recovered via\n * `readEntry`. */\n buildPluginData: (input: { legacyId: string; slug: string; itemId: string; priority: CompletionPriority; navigateTarget: string }) => unknown;\n /** Recognise a bell entry produced by this reconciler and recover its\n * internal key + stored priority. Returns null for entries that didn't\n * originate here, so `listAll()` scans skip foreign entries. */\n readEntry: (pluginData: unknown) => { legacyId: string; priority: CompletionPriority } | null;\n}\n\nconst NOOP_LOG: CollectionWatcherLogger = { info: () => {}, warn: () => {} };\n\nlet adapter: CollectionNotificationAdapter | null = null;\nlet activeLogger: CollectionWatcherLogger = NOOP_LOG;\n\n/** Wire the host adapter + logger. Call once at startup, before\n * `startCollectionWatchers` or any direct reconcile call. */\nexport function configureCollectionWatchers(config: { adapter: CollectionNotificationAdapter; log?: CollectionWatcherLogger }): void {\n ({ adapter } = config);\n activeLogger = config.log ?? NOOP_LOG;\n}\n\nexport function requireAdapter(): CollectionNotificationAdapter {\n if (!adapter) throw new Error(\"collection-watchers: configureCollectionWatchers() not called\");\n return adapter;\n}\n\nexport function log(): CollectionWatcherLogger {\n return activeLogger;\n}\n\n/** Test-only: clear the host wiring. */\nexport function resetCollectionWatchersConfig(): void {\n adapter = null;\n activeLogger = NOOP_LOG;\n}\n\nexport function errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","// Evaluation-only injectable clock. When MULMOCLAUDE_FAKE_NOW is set to\n// a parseable date/datetime string, every reconcile pass derives \"now\"\n// from it instead of the wall clock — letting evaluation runs and tests\n// advance time deterministically (a `triggerField` coming due, a `spawn`\n// successor's own trigger firing later) without waiting for real days to\n// pass. Unset or unparseable (i.e. normal operation) → real wall clock.\n// Read per call, so a long-lived server picks up changes without restart.\nexport function evalNow(): Date {\n const raw = process.env.MULMOCLAUDE_FAKE_NOW;\n if (!raw) return new Date();\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? new Date() : parsed;\n}\n","// Bell-notification reconciler for collections whose schema declares\n// `completionField`. Driven by `watcher.ts`, which calls into the\n// functions below on file-system events and on boot.\n//\n// The model is **convergent**: a watcher event re-reads the record from\n// disk and the reconciler enforces the invariant\n//\n// bell entry exists for (slug, itemId) ↔\n// schema has completionField ∧\n// file exists ∧\n// `String(item[completionField])` ∉ completionDoneValues\n// (∧ trigger due ∧ notifyWhen matches, when declared)\n//\n// Each reconcile is idempotent (`ensure*` / `clear*` no-op when state\n// already matches). This is why event-type quirks of `fs.watch`\n// (`rename` vs `change`, missed events, atomic-write coalescence) don't\n// matter — every event re-derives the desired state from the file.\n//\n// Lookup uses a deterministic internal `legacyId` derived from\n// `<slug>:<itemId>`, stashed on each entry's `pluginData` via the host\n// adapter, so the clear path and the dedup check both find the entry\n// without a side state file.\n\nimport { clear as notifierClear, listAll, publish as notifierPublish, updateForPlugin as notifierUpdate, type NotifierEntry } from \"../notifier\";\nimport { whenMatches, type CollectionItem, type CollectionSchema } from \"../collection\";\nimport { type DiscoveryOptions, listItems, readItem, type IoOptions, isTriggerDue, maybeSpawnSuccessor, loadCollection } from \"../collection/server\";\nimport { type CompletionPriority, errMsg, log, requireAdapter } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\n\n/** The internal-id prefix every collection-completion bell entry carries.\n * Used both to build new keys and to filter sweep candidates from the\n * active bell. */\nconst LEGACY_ID_PREFIX = \"collection-completion:\";\n\n/** Stable key encoding slug + item, round-tripped through the entry's\n * `pluginData` so we can find it later without a side state file. Slug +\n * itemId are upstream-validated via `safeSlugName`, which forbids the\n * colon separator, so the two-segment parse below is unambiguous. */\nfunction completionLegacyId(slug: string, itemId: string): string {\n return `${LEGACY_ID_PREFIX}${slug}:${itemId}`;\n}\n\n/** Decode a key back into its (slug, itemId) pair, or null if the string\n * didn't originate from this module. Used by the sweep step. */\nfunction parseCompletionLegacyId(legacyId: string): { slug: string; itemId: string } | null {\n if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;\n const body = legacyId.slice(LEGACY_ID_PREFIX.length);\n const colon = body.indexOf(\":\");\n if (colon < 0) return null;\n return { slug: body.slice(0, colon), itemId: body.slice(colon + 1) };\n}\n\n/** The human-readable label shown in a completion notification's title.\n * Uses the schema's `displayField` value when declared and non-empty;\n * otherwise falls back to the record's primaryKey (`itemId`). */\nexport function resolveDisplayLabel(schema: CollectionSchema, item: CollectionItem, itemId: string): string {\n const { displayField } = schema;\n if (!displayField) return itemId;\n const raw = item[displayField];\n if (raw === undefined || raw === null) return itemId;\n const label = String(raw).trim();\n return label.length > 0 ? label : itemId;\n}\n\n/** True iff the schema declares completion tracking AND the item's\n * `completionField` value (stringified) is in `completionDoneValues`. */\nexport function itemIsDone(schema: CollectionSchema, item: CollectionItem): boolean {\n const { completionField, completionDoneValues } = schema;\n if (!completionField || !completionDoneValues) return false;\n const raw = item[completionField];\n if (raw === undefined || raw === null) return false;\n return completionDoneValues.includes(String(raw));\n}\n\n/** Every active bell entry whose key matches this (slug, itemId).\n * Returns multiple when defensive cleanup is needed. Scans `listAll()`\n * — cheap because the active set is bounded. */\nasync function findActiveEntries(slug: string, itemId: string): Promise<NotifierEntry[]> {\n const adapter = requireAdapter();\n const legacyId = completionLegacyId(slug, itemId);\n const entries = await listAll();\n return entries.filter((entry) => adapter.readEntry(entry.pluginData)?.legacyId === legacyId);\n}\n\nasync function findActiveEntryIds(slug: string, itemId: string): Promise<string[]> {\n return (await findActiveEntries(slug, itemId)).map((entry) => entry.id);\n}\n\n/** Per-key in-flight lock. Serializes concurrent `ensureItemNotification`\n * calls for the same (slug, itemId) so the `findActiveEntries → publish`\n * check stays atomic across callers — not just across watcher events.\n * `listAll` bypasses the engine's write queue, so without this lock two\n * reconcile paths (a watcher event + a `reconcileAllItems` pass that a\n * readdir-triggered event raced) could both miss each other's in-flight\n * publish and produce duplicate entries. */\ninterface EnsureLock {\n promise: Promise<void>;\n}\nconst ensureLocks = new Map<string, EnsureLock>();\n\n/** Bell priority for a record: the FIRST flagged value in `notifyWhen.in`\n * (most urgent) reads `high`, every other flagged value `normal`.\n * Collections with no `notifyWhen` (notify for every open record) stay\n * `normal`. */\nfunction notifyPriorityForItem(schema: CollectionSchema, item: CollectionItem): CompletionPriority {\n const spec = schema.notifyWhen;\n if (!spec) return \"normal\";\n const value = item[spec.field] === undefined || item[spec.field] === null ? \"\" : String(item[spec.field]);\n return spec.in.indexOf(value) === 0 ? \"high\" : \"normal\";\n}\n\nasync function ensureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const legacyId = completionLegacyId(slug, itemId);\n // Drain any in-flight publish for this key BEFORE our check + set. The\n // drain + claim runs synchronously between `ensureLocks.get` and\n // `ensureLocks.set`, so two callers can't both observe an empty slot.\n\n while (true) {\n const inflight = ensureLocks.get(legacyId);\n if (!inflight) break;\n await inflight.promise;\n }\n const lock: EnsureLock = { promise: doEnsureItemNotification(slug, schema, itemId, legacyId, displayLabel, priority) };\n ensureLocks.set(legacyId, lock);\n try {\n await lock.promise;\n } finally {\n // Only clear the slot if it still points at OUR lock — a\n // sufficiently-delayed cleanup must not stomp a later claim.\n if (ensureLocks.get(legacyId) === lock) {\n ensureLocks.delete(legacyId);\n }\n }\n}\n\n/** Converge any already-present bell entries to `priority`, updating in\n * place (preserving id / position / createdAt) so a record whose flagged\n * value changed while it stayed pending re-colours the bell without a\n * clear+republish flicker. No-op when the stored priority already matches. */\nasync function reconcileEntrySeverity(slug: string, itemId: string, entries: NotifierEntry[], priority: CompletionPriority): Promise<void> {\n const adapter = requireAdapter();\n for (const entry of entries) {\n const parsed = adapter.readEntry(entry.pluginData);\n if (!parsed || parsed.priority === priority) continue;\n await notifierUpdate(adapter.pluginPkg, entry.id, {\n severity: adapter.priorityToSeverity(priority),\n pluginData: adapter.buildPluginData({\n legacyId: parsed.legacyId,\n slug,\n itemId,\n priority,\n navigateTarget: adapter.buildNavigateTarget(slug, itemId),\n }),\n });\n }\n}\n\nasync function doEnsureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n legacyId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const adapter = requireAdapter();\n try {\n const existing = await findActiveEntries(slug, itemId);\n if (existing.length > 0) {\n await reconcileEntrySeverity(slug, itemId, existing, priority);\n return;\n }\n const navigateTarget = adapter.buildNavigateTarget(slug, itemId);\n // `lifecycle: \"action\"` — these are state-of-the-world entries\n // mirroring an outstanding obligation (the item is pending), not\n // transient pings. Validation requires a non-info severity and a\n // non-empty `navigateTarget` (the slug + itemId deep-link).\n await notifierPublish({\n pluginPkg: adapter.pluginPkg,\n severity: adapter.priorityToSeverity(priority),\n lifecycle: \"action\",\n title: `${schema.title}: ${displayLabel}`,\n navigateTarget,\n pluginData: adapter.buildPluginData({ legacyId, slug, itemId, priority, navigateTarget }),\n });\n } catch (err) {\n log().warn(\"notify ensure failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Idempotently clear EVERY bell entry that matches this (slug, itemId).\n * Silent no-op when nothing matches. The \"every\" is defensive: if a\n * duplicate ever slips through, this drains the lot. */\nexport async function clearItemNotification(slug: string, itemId: string): Promise<void> {\n try {\n const ids = await findActiveEntryIds(slug, itemId);\n for (const entryId of ids) {\n await notifierClear(entryId);\n }\n } catch (err) {\n log().warn(\"notify clear failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Reconcile one item to the desired bell state. Re-reads the record from\n * disk so the decision is grounded in current truth, not in the event\n * payload. Safe to call when the file is missing (delete path).\n *\n * `ioOpts` flows into `readItem`'s workspace-containment check —\n * production callers (the watcher) pass nothing; tests pass\n * `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */\nexport async function reconcileItem(\n slug: string,\n schema: CollectionSchema,\n dataDir: string,\n itemId: string,\n ioOpts: IoOptions = {},\n now: Date = evalNow(),\n): Promise<void> {\n if (!schema.completionField) {\n // Schema doesn't track completion — drop any stale entry.\n await clearItemNotification(slug, itemId);\n return;\n }\n const item = await readItem(dataDir, itemId, ioOpts);\n if (item === null) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Recurrence: predicate-gated + create-if-absent, idempotent and\n // independent of this item's own bell state. Runs before the done-clear\n // below so marking an item done still spawns its successor.\n await maybeSpawnSuccessor(slug, schema, dataDir, item, itemId, ioOpts);\n if (itemIsDone(schema, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Time gate: when the schema declares `triggerField`, suppress the bell\n // until the clock reaches that date (minus `triggerLeadDays`).\n // Unparseable date ⇒ fail safe (no bell); warn ONLY when the field carries\n // a non-empty value that won't parse — an empty optional trigger date is a\n // normal state and must not spam a WARN every reconcile tick.\n if (schema.triggerField) {\n const triggerRaw = item[schema.triggerField];\n const due = isTriggerDue(triggerRaw, now, schema.triggerLeadDays);\n const isEmpty = triggerRaw === undefined || triggerRaw === null || triggerRaw === \"\";\n if (due === null && !isEmpty) {\n log().warn(\"trigger date unparseable, suppressing bell\", { slug, itemId, triggerField: schema.triggerField });\n }\n if (due !== true) {\n await clearItemNotification(slug, itemId);\n return;\n }\n }\n // Condition gate: when the schema declares `notifyWhen`, only bell\n // records matching the predicate. Convergent — a record that stops\n // matching has its bell cleared.\n if (!whenMatches(schema.notifyWhen, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));\n}\n\n/** Boot-time reconcile: walk every record under `dataDir` once and\n * reconcile it. Catches up changes that happened while the server was\n * down. Deleted items are covered by `sweepStaleActiveEntries`, not this\n * function (it only sees files that exist). */\nexport async function reconcileAllItems(slug: string, schema: CollectionSchema, dataDir: string, ioOpts: IoOptions = {}, now: Date = evalNow()): Promise<void> {\n if (!schema.completionField) return;\n let items: CollectionItem[];\n try {\n items = await listItems(dataDir, ioOpts);\n } catch (err) {\n log().warn(\"reconcile list failed\", { slug, dataDir, error: errMsg(err) });\n return;\n }\n const { primaryKey } = schema;\n for (const item of items) {\n const raw = item[primaryKey];\n if (typeof raw !== \"string\" || raw.length === 0) continue;\n await reconcileItem(slug, schema, dataDir, raw, ioOpts, now);\n }\n}\n\n/** Boot-time sweep over the active bell: drop any entries whose underlying\n * file is gone, whose collection was deleted, whose schema no longer\n * tracks completion, or whose item is now done. Reverse-covers the cases\n * `reconcileAllItems` misses (it only walks files that exist). */\nexport async function sweepStaleActiveEntries(opts: DiscoveryOptions = {}): Promise<void> {\n const adapter = requireAdapter();\n let entries;\n try {\n entries = await listAll();\n } catch (err) {\n log().warn(\"sweep list failed\", { error: errMsg(err) });\n return;\n }\n for (const entry of entries) {\n const own = adapter.readEntry(entry.pluginData);\n if (!own) continue;\n const parsed = parseCompletionLegacyId(own.legacyId);\n if (!parsed) continue;\n const { slug, itemId } = parsed;\n try {\n const collection = await loadCollection(slug, opts);\n if (!collection || !collection.schema.completionField) {\n await notifierClear(entry.id);\n continue;\n }\n const item = await readItem(collection.dataDir, itemId, opts);\n if (item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item)) {\n await notifierClear(entry.id);\n }\n } catch (err) {\n log().warn(\"sweep entry failed\", { slug, itemId, error: errMsg(err) });\n }\n }\n}\n\n/** Test-only: clear the per-key in-flight locks. */\nexport function _resetReconcilerLocksForTesting(): void {\n ensureLocks.clear();\n}\n","// Filesystem watchers that drive collection-completion bell\n// notifications. One `fs.watch` per discovered collection's `dataDir`,\n// fanned out from a single boot call + a 30-second re-discovery interval\n// that catches newly-created / deleted collections (there is no\n// in-process \"collections changed\" event broadcast).\n//\n// Why a watcher, not just route hooks: the canonical pattern for\n// collection-skills has the agent Write records directly with the Write\n// tool — that path never hits the REST API, so a route-level hook would\n// miss most of the traffic the user generates. The watcher catches every\n// mutation regardless of who wrote the file.\n//\n// All decisions live in `reconciler.ts`; this module is pure plumbing:\n// discover, mkdir, fs.watch, forward events into the reconciler. Every\n// reconcile call is idempotent so fs.watch's well-known quirks (`rename`\n// vs `change`, atomic-write coalescence, filename === null on some\n// platforms) don't need special handling.\n\nimport { watch, type FSWatcher } from \"node:fs\";\nimport { mkdir } from \"node:fs/promises\";\nimport { discoverCollections, loadCollection, type DiscoveryOptions, type LoadedCollection } from \"../collection/server\";\nimport type { CollectionSchema } from \"../collection\";\nimport { errMsg, log } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\nimport { reconcileAllItems, reconcileItem, sweepStaleActiveEntries } from \"./reconciler.js\";\n\n// Collections don't get added / removed rapidly; 30 s is a comfortable\n// upper bound on how long a new schema can sit before its watcher is up.\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\nconst REDISCOVERY_INTERVAL_MS = 30 * ONE_SECOND_MS;\n\n// Wall-clock tick that re-reconciles time-dependent collections (those\n// declaring `triggerField` and/or `spawn`). The fs.watcher only re-runs\n// the reconciler on FILE changes; a `triggerField` bell that should fire\n// \"when the clock reaches date X\" — and a `spawn` whose successor's own\n// trigger later comes due — change no file at that moment, so a periodic\n// re-derivation is required.\nconst TRIGGER_TICK_INTERVAL_MS = ONE_MINUTE_MS;\n\ninterface CollectionWatcher {\n slug: string;\n dataDir: string;\n watcher: FSWatcher;\n /** Last-seen serialized schema for change detection. When a rediscovery\n * tick observes a different value, the watcher's items are reconciled\n * and the cache is refreshed — this catches schema-only edits (e.g.\n * flipping `completionField` on or off) that don't touch any record\n * file and would otherwise leave bell state stale indefinitely. */\n schemaJson: string;\n}\n\nconst watchers = new Map<string, CollectionWatcher>();\nlet rediscoveryTimer: ReturnType<typeof setInterval> | null = null;\nlet triggerTimer: ReturnType<typeof setInterval> | null = null;\nlet started = false;\n/** Discovery options threaded into every `discoverCollections` /\n * `loadCollection` / `sweepStaleActiveEntries` call. Production: empty\n * (live workspace). Tests: `{ workspaceRoot, userSkillsDir }` pointing\n * at a fixture tree. Module-level so per-event handlers can read it\n * without threading through every signature. */\nlet discoveryOpts: DiscoveryOptions = {};\n\n/** Per-key single-flight slot (declared here so `stopCollectionWatchers`\n * can clear it during teardown). */\ninterface ReconcileSlot {\n running: Promise<void>;\n pending: boolean;\n}\nconst itemSlots = new Map<string, ReconcileSlot>();\n\n/** Test-only configuration knobs. Production callers pass nothing and get\n * the live workspace defaults; tests pass a tmpdir-rooted `discoveryOpts`\n * and override the tick cadences (or set them to `null` to disable the\n * auto-ticks so the test drives sync manually). */\nexport interface CollectionWatcherOptions {\n discoveryOpts?: DiscoveryOptions;\n rediscoveryIntervalMs?: number | null;\n triggerTickIntervalMs?: number | null;\n}\n\n/** Boot entry point: sweep stale active entries, then mount watchers for\n * every discovered collection and arm the periodic re-discovery poll.\n * Idempotent — a second call is a no-op. */\nexport async function startCollectionWatchers(opts: CollectionWatcherOptions = {}): Promise<void> {\n if (started) return;\n // `started` only flips on AFTER boot finishes. If sweep or syncWatchers\n // throws mid-boot, reset state on failure so a supervisor / test\n // harness can retry instead of being permanently latched.\n discoveryOpts = opts.discoveryOpts ?? {};\n try {\n // Boot reconcile is split in two: sweep first (drop bell entries whose\n // files / collections / schemas vanished while the server was down),\n // then `syncWatchers` runs the per-collection forward fill. Both paths\n // are idempotent and converge on the same end state.\n await sweepStaleActiveEntries(discoveryOpts);\n await syncWatchers();\n const intervalMs = opts.rediscoveryIntervalMs === undefined ? REDISCOVERY_INTERVAL_MS : opts.rediscoveryIntervalMs;\n if (intervalMs !== null) {\n rediscoveryTimer = setInterval(() => {\n syncWatchers().catch((err: unknown) => {\n log().warn(\"watcher rediscovery failed\", { error: errMsg(err) });\n });\n }, intervalMs);\n // `unref` so a clean process exit isn't blocked waiting for the tick.\n rediscoveryTimer.unref();\n }\n const triggerMs = opts.triggerTickIntervalMs === undefined ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;\n if (triggerMs !== null) {\n triggerTimer = setInterval(() => {\n tickTimeTriggers().catch((err: unknown) => {\n log().warn(\"watcher trigger tick failed\", { error: errMsg(err) });\n });\n }, triggerMs);\n triggerTimer.unref();\n }\n started = true;\n } catch (err) {\n discoveryOpts = {};\n throw err;\n }\n}\n\n/** Tear down every watcher and stop the intervals. Used by tests;\n * production never calls this (process exit reclaims the fds). Resets\n * `started` so a subsequent `startCollectionWatchers` re-mounts. */\nexport async function stopCollectionWatchers(): Promise<void> {\n if (rediscoveryTimer) {\n clearInterval(rediscoveryTimer);\n rediscoveryTimer = null;\n }\n if (triggerTimer) {\n clearInterval(triggerTimer);\n triggerTimer = null;\n }\n for (const watcher of watchers.values()) {\n try {\n watcher.watcher.close();\n } catch {\n /* fs.watch close is best-effort */\n }\n }\n watchers.clear();\n itemSlots.clear();\n discoveryOpts = {};\n started = false;\n}\n\n/** Test-only: manually trigger one rediscovery + reconcile pass. */\nexport async function _syncWatchersForTesting(): Promise<void> {\n await syncWatchers();\n}\n\n/** Test-only: drive one wall-clock tick synchronously, with an optional\n * injected clock. */\nexport async function _tickTimeTriggersForTesting(now?: Date): Promise<void> {\n await tickTimeTriggers(now);\n}\n\n/** Re-reconcile every watched collection that depends on the clock — i.e.\n * declares `triggerField` (a bell that fires at a date) and/or `spawn`\n * (recurrence whose successors come due over time). Collections with\n * neither are skipped. Idempotent. The schema is parsed back from the\n * watcher's cached `schemaJson` to avoid a per-tick disk read. */\nasync function tickTimeTriggers(now: Date = evalNow()): Promise<void> {\n for (const entry of watchers.values()) {\n let schema: CollectionSchema;\n try {\n schema = JSON.parse(entry.schemaJson) as CollectionSchema;\n } catch (err) {\n log().warn(\"trigger tick: bad cached schema\", { slug: entry.slug, error: errMsg(err) });\n continue;\n }\n if (!schema.triggerField && !schema.spawn) continue;\n await reconcileAllItems(entry.slug, schema, entry.dataDir, discoveryOpts, now);\n }\n}\n\n/** Reconcile the watcher set against the currently-discovered\n * collections. Adds watchers for new slugs (with a boot reconcile of\n * their items), drops watchers for vanished slugs, and re-reconciles\n * items for collections whose schema changed. Runs a final sweep when\n * this tick changed the watcher set or any schema. */\nasync function syncWatchers(): Promise<void> {\n let collections;\n try {\n collections = await discoverCollections(discoveryOpts);\n } catch (err) {\n log().warn(\"watcher discover failed\", { error: errMsg(err) });\n return;\n }\n const liveSlugs = new Set(collections.map((collection) => collection.slug));\n const vanishedMutated = stopVanishedWatchers(liveSlugs);\n const schemaMutated = await reconcileChangedSchemas(collections);\n const addedMutated = await startNewWatchers(collections);\n if (vanishedMutated || schemaMutated || addedMutated) {\n await sweepStaleActiveEntries(discoveryOpts);\n }\n}\n\nfunction stopVanishedWatchers(liveSlugs: Set<string>): boolean {\n let mutated = false;\n for (const slug of [...watchers.keys()]) {\n if (liveSlugs.has(slug)) continue;\n const watcher = watchers.get(slug);\n if (watcher) {\n try {\n watcher.watcher.close();\n } catch {\n /* best-effort */\n }\n }\n watchers.delete(slug);\n mutated = true;\n log().info(\"watcher stopped\", { slug });\n }\n return mutated;\n}\n\n/** Re-reconcile already-watched collections whose schema changed since\n * the last tick. New collections fall through to `startNewWatchers`. */\nasync function reconcileChangedSchemas(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n const existing = watchers.get(collection.slug);\n if (!existing) continue;\n const nextJson = JSON.stringify(collection.schema);\n if (existing.schemaJson === nextJson) continue;\n existing.schemaJson = nextJson;\n log().info(\"watcher schema changed, re-reconciling\", { slug: collection.slug });\n await reconcileAllItems(collection.slug, collection.schema, collection.dataDir, discoveryOpts);\n mutated = true;\n }\n return mutated;\n}\n\nasync function startNewWatchers(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n if (watchers.has(collection.slug)) continue;\n await startWatcherFor(collection.slug, collection.schema, collection.dataDir);\n mutated = true;\n }\n return mutated;\n}\n\nasync function startWatcherFor(slug: string, schema: CollectionSchema, dataDir: string): Promise<void> {\n try {\n // `fs.watch` throws on a missing dir, so ensure it exists. New\n // collections legitimately start with no records — mkdir is the\n // canonical first-use bootstrap.\n await mkdir(dataDir, { recursive: true });\n // Boot reconcile this collection's existing items BEFORE mounting the\n // watcher: a pending item the user added during downtime needs its\n // bell entry even if no event fires today.\n await reconcileAllItems(slug, schema, dataDir, discoveryOpts);\n const watcher = watch(dataDir, { persistent: false }, (_eventType, filename) => {\n // Errors from inside the callback would propagate as unhandled\n // rejections — wrap so a single bad event can't unwind the watcher.\n onEvent(slug, filename).catch((err: unknown) => {\n log().warn(\"watcher event failed\", { slug, filename, error: errMsg(err) });\n });\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"watcher error\", { slug, error: errMsg(err) });\n });\n watchers.set(slug, { slug, dataDir, watcher, schemaJson: JSON.stringify(schema) });\n log().info(\"watcher started\", { slug, dataDir });\n } catch (err) {\n log().warn(\"watcher start failed\", { slug, error: errMsg(err) });\n }\n}\n\n/** Test-only: the per-key single-flight scheduler. Exported so test code\n * can drive rapid-fire calls directly and observe the trailing coalesce\n * — `fs.watch` event timing is too flaky to assert against.\n *\n * Single-flight semantics: while a reconcile is in flight for a given\n * (slug, itemId), additional events on the same key set `pending = true`\n * and return — the running reconcile re-runs once after it completes.\n * This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as\n * 2-3 events) into a single reconcile + one trailing re-run. */\nexport function _scheduleItemReconcileForTesting(slug: string, schema: CollectionSchema, dataDir: string, itemId: string): Promise<void> {\n return scheduleItemReconcile(slug, schema, dataDir, itemId);\n}\n\nfunction scheduleItemReconcile(slug: string, schema: CollectionSchema, dataDir: string, itemId: string): Promise<void> {\n const key = `${slug}\\x00${itemId}`;\n const existing = itemSlots.get(key);\n if (existing) {\n existing.pending = true;\n return existing.running;\n }\n const slot: ReconcileSlot = { running: Promise.resolve(), pending: false };\n slot.running = (async () => {\n try {\n // Re-run while events keep arriving — the trailing re-run captures\n // any state change that landed during a prior pass. After each pass\n // we read `pending` and zero it before the next iteration, so an\n // event that fires *during* the last reconcile's await still\n // triggers one more pass before the slot is freed.\n let keepGoing = true;\n while (keepGoing) {\n slot.pending = false;\n await reconcileItem(slug, schema, dataDir, itemId, discoveryOpts);\n keepGoing = slot.pending;\n }\n } finally {\n itemSlots.delete(key);\n }\n })();\n itemSlots.set(key, slot);\n return slot.running;\n}\n\n/** Handle a single fs.watch event. Re-loads the collection (schema may\n * have changed since startup), filters out non-record files, and\n * forwards to the single-flighted reconciler. `filename === null` (rare,\n * platform-specific) triggers a full directory rescan to be safe. */\nasync function onEvent(slug: string, filename: string | Buffer | null): Promise<void> {\n const collection = await loadCollection(slug, discoveryOpts);\n if (!collection) return;\n if (filename === null) {\n // Some platforms omit the filename on a watch event — we don't know\n // which record changed. `reconcileAllItems` covers items whose file\n // still exists; pair it with a sweep so any record deleted inside the\n // same opaque event has its stale bell entry cleared too.\n await reconcileAllItems(slug, collection.schema, collection.dataDir, discoveryOpts);\n await sweepStaleActiveEntries(discoveryOpts);\n return;\n }\n const name = typeof filename === \"string\" ? filename : filename.toString(\"utf-8\");\n // Filter: only record files (`*.json`), skip dot-prefixed (atomic\n // writes / OS metadata / editor swap files). The reconciler is\n // idempotent so a stray non-record event would be harmless, but\n // skipping early avoids needless I/O.\n if (!name.endsWith(\".json\") || name.startsWith(\".\")) return;\n const itemId = name.slice(0, -\".json\".length);\n await scheduleItemReconcile(slug, collection.schema, collection.dataDir, itemId);\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
- import { whenMatches } from "../collection/index.js";
2
- import { I as readItem, M as listItems, c as isTriggerDue, l as maybeSpawnSuccessor, v as discoverCollections, y as loadCollection } from "../server-Cc6XcyOA.js";
1
+ import { k as whenMatches } from "../calendarGrid-B3dtFN-m.js";
2
+ import "../collection/index.js";
3
+ import { B as readItem, I as listItems, b as discoverCollections, c as isTriggerDue, l as maybeSpawnSuccessor, x as loadCollection } from "../server-0IVR-Y_G.js";
3
4
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
4
5
  import { watch } from "node:fs";
5
6
  import { mkdir } from "node:fs/promises";