@mulmoclaude/core 1.12.0 → 1.14.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 (117) hide show
  1. package/assets/helps/error-recovery.md +110 -1
  2. package/dist/{calendarGrid-CaS9er8i.cjs → calendarGrid-BQYqOCnF.cjs} +8 -9
  3. package/dist/{calendarGrid-CaS9er8i.cjs.map → calendarGrid-BQYqOCnF.cjs.map} +1 -1
  4. package/dist/{calendarGrid-CrGxeCOG.js → calendarGrid-DivcNKkX.js} +8 -9
  5. package/dist/{calendarGrid-CrGxeCOG.js.map → calendarGrid-DivcNKkX.js.map} +1 -1
  6. package/dist/collection/core/project.d.ts +11 -0
  7. package/dist/collection/core/schemaZ.d.ts +4 -4
  8. package/dist/collection/index.cjs +5 -4
  9. package/dist/collection/index.js +5 -5
  10. package/dist/collection/registry/server/index.cjs +3 -3
  11. package/dist/collection/registry/server/index.cjs.map +1 -1
  12. package/dist/collection/registry/server/index.js +3 -3
  13. package/dist/collection/registry/server/index.js.map +1 -1
  14. package/dist/collection/server/csvQuery.d.ts +7 -2
  15. package/dist/collection/server/csvStore.d.ts +2 -1
  16. package/dist/collection/server/index.cjs +3 -3
  17. package/dist/collection/server/index.js +3 -3
  18. package/dist/collection-watchers/index.cjs +10 -25
  19. package/dist/collection-watchers/index.cjs.map +1 -1
  20. package/dist/collection-watchers/index.js +10 -25
  21. package/dist/collection-watchers/index.js.map +1 -1
  22. package/dist/{discovery-NRy3tyUA.cjs → discovery-CRDx6TS4.cjs} +88 -47
  23. package/dist/discovery-CRDx6TS4.cjs.map +1 -0
  24. package/dist/{discovery-B4CZvrXR.js → discovery-D6p4ITNR.js} +88 -47
  25. package/dist/discovery-D6p4ITNR.js.map +1 -0
  26. package/dist/{dist-pWpC-b04.cjs → dist-CsgSfWwR.cjs} +66 -1
  27. package/dist/{dist-pWpC-b04.cjs.map → dist-CsgSfWwR.cjs.map} +1 -1
  28. package/dist/{dist-Cwk0e12G.js → dist-D8zokgGo.js} +25 -2
  29. package/dist/{dist-Cwk0e12G.js.map → dist-D8zokgGo.js.map} +1 -1
  30. package/dist/feeds/index.cjs +2 -2
  31. package/dist/feeds/index.js +2 -2
  32. package/dist/feeds/server/engine.d.ts +27 -0
  33. package/dist/feeds/server/index.cjs +97 -26
  34. package/dist/feeds/server/index.cjs.map +1 -1
  35. package/dist/feeds/server/index.d.ts +1 -1
  36. package/dist/feeds/server/index.js +96 -27
  37. package/dist/feeds/server/index.js.map +1 -1
  38. package/dist/google/calendar.d.ts +23 -9
  39. package/dist/google/calendarStateLock.d.ts +23 -0
  40. package/dist/google/collectionProjection.d.ts +0 -8
  41. package/dist/google/collectionPush.d.ts +13 -8
  42. package/dist/google/collectionSync.d.ts +80 -10
  43. package/dist/google/index.cjs +366 -98
  44. package/dist/google/index.cjs.map +1 -1
  45. package/dist/google/index.d.ts +6 -4
  46. package/dist/google/index.js +357 -99
  47. package/dist/google/index.js.map +1 -1
  48. package/dist/{ingestTypes-D1GQdG8e.cjs → ingestTypes-C7IJSqv4.cjs} +2 -2
  49. package/dist/{ingestTypes-D1GQdG8e.cjs.map → ingestTypes-C7IJSqv4.cjs.map} +1 -1
  50. package/dist/{ingestTypes-XReG-li7.js → ingestTypes-DmtDLRmt.js} +2 -2
  51. package/dist/{ingestTypes-XReG-li7.js.map → ingestTypes-DmtDLRmt.js.map} +1 -1
  52. package/dist/notifier/index.cjs +1 -1
  53. package/dist/notifier/index.js +1 -1
  54. package/dist/notifier/store.d.ts +4 -4
  55. package/dist/{notifier-BdA5qzhe.js → notifier-Dxh8mEFh.js} +39 -25
  56. package/dist/notifier-Dxh8mEFh.js.map +1 -0
  57. package/dist/{notifier-tMsAXyXp.cjs → notifier-Wc4M24Bi.cjs} +39 -25
  58. package/dist/notifier-Wc4M24Bi.cjs.map +1 -0
  59. package/dist/plugin-vue/index.cjs +4 -2
  60. package/dist/plugin-vue/index.cjs.map +1 -1
  61. package/dist/plugin-vue/index.js +4 -2
  62. package/dist/plugin-vue/index.js.map +1 -1
  63. package/dist/project-B0j_Vf_Q.js +33 -0
  64. package/dist/project-B0j_Vf_Q.js.map +1 -0
  65. package/dist/project-C1ep9pvo.cjs +44 -0
  66. package/dist/project-C1ep9pvo.cjs.map +1 -0
  67. package/dist/{promptSafety-FD8cQn3e.cjs → promptSafety-B52GvaJc.cjs} +99 -85
  68. package/dist/promptSafety-B52GvaJc.cjs.map +1 -0
  69. package/dist/{promptSafety-C--op_6a.js → promptSafety-DJgaDjM4.js} +99 -85
  70. package/dist/promptSafety-DJgaDjM4.js.map +1 -0
  71. package/dist/remote-host/index.cjs +2 -1
  72. package/dist/remote-host/index.d.ts +10 -0
  73. package/dist/remote-host/index.js +2 -2
  74. package/dist/remote-host/server/index.cjs +2 -2
  75. package/dist/remote-host/server/index.cjs.map +1 -1
  76. package/dist/remote-host/server/index.js +2 -2
  77. package/dist/remote-host/server/index.js.map +1 -1
  78. package/dist/{remote-host-DkDVxNim.cjs → remote-host-CtQjagPt.cjs} +53 -1
  79. package/dist/remote-host-CtQjagPt.cjs.map +1 -0
  80. package/dist/remote-host-Dba4lF3Z.js +82 -0
  81. package/dist/remote-host-Dba4lF3Z.js.map +1 -0
  82. package/dist/remote-view/index.cjs +6 -4
  83. package/dist/remote-view/index.cjs.map +1 -1
  84. package/dist/remote-view/index.js +6 -4
  85. package/dist/remote-view/index.js.map +1 -1
  86. package/dist/scheduler/index.cjs +1 -1
  87. package/dist/scheduler/index.js +1 -1
  88. package/dist/{server-DdyIvFhz.js → server-D6EUf3BF.js} +46 -27
  89. package/dist/server-D6EUf3BF.js.map +1 -0
  90. package/dist/{server-7U-3DE2e.cjs → server-D7_oRbTJ.cjs} +46 -27
  91. package/dist/server-D7_oRbTJ.cjs.map +1 -0
  92. package/dist/utils/index.cjs +1 -1
  93. package/dist/utils/index.js +1 -1
  94. package/dist/whisper/index.cjs +2 -6
  95. package/dist/whisper/index.cjs.map +1 -1
  96. package/dist/whisper/index.js +2 -6
  97. package/dist/whisper/index.js.map +1 -1
  98. package/dist/wiki/index.cjs +2 -2
  99. package/dist/wiki/index.cjs.map +1 -1
  100. package/dist/wiki/index.js +2 -2
  101. package/dist/wiki/index.js.map +1 -1
  102. package/package.json +2 -2
  103. package/dist/discovery-B4CZvrXR.js.map +0 -1
  104. package/dist/discovery-NRy3tyUA.cjs.map +0 -1
  105. package/dist/notifier-BdA5qzhe.js.map +0 -1
  106. package/dist/notifier-tMsAXyXp.cjs.map +0 -1
  107. package/dist/project-BWI5w_BT.cjs +0 -17
  108. package/dist/project-BWI5w_BT.cjs.map +0 -1
  109. package/dist/project-bU98ycsy.js +0 -12
  110. package/dist/project-bU98ycsy.js.map +0 -1
  111. package/dist/promptSafety-C--op_6a.js.map +0 -1
  112. package/dist/promptSafety-FD8cQn3e.cjs.map +0 -1
  113. package/dist/remote-host-D_BRFHcI.js +0 -36
  114. package/dist/remote-host-D_BRFHcI.js.map +0 -1
  115. package/dist/remote-host-DkDVxNim.cjs.map +0 -1
  116. package/dist/server-7U-3DE2e.cjs.map +0 -1
  117. package/dist/server-DdyIvFhz.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"notifier-BdA5qzhe.js","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. No deep-link target.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules are mirrored in the HTTP layer so plugin-runtime callers\n * and HTTP callers hit the same wall. */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`). The\n * engine never inspects it — used only for `listFor()` filtering\n * and as a UI grouping key. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, with `&notificationId=<id>` appended\n * so the landing page can identify which entry to clear. The\n * engine doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string;\n lifecycle?: NotifierLifecycle;\n navigateTarget?: string;\n pluginData?: TPluginData;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap. The bell popup's History section renders this\n * many entries; older ones fall off when new terminations land. */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport type { NotifierFile, NotifierHistoryFile } from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nfunction isNotFoundError(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet (first ever call on a fresh workspace). Any other\n * read or parse failure throws — the caller has to decide whether to\n * surface or recover, since silently treating \"malformed file\" as\n * \"no entries\" would lose data. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: {} };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n // `typeof null === \"object\"` and `Array.isArray([])` is also true,\n // so a naive `typeof entries !== \"object\"` check would let\n // `{ entries: null }` and `{ entries: [] }` through, which then\n // crash downstream `engine.get` / `list*` mutations. Reject both\n // shapes here at load time so the failure surfaces as a clear\n // \"malformed file\" error.\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n const { entries } = parsed as { entries: unknown };\n if (typeof entries !== \"object\" || entries === null || Array.isArray(entries)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n return parsed as NotifierFile;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: [] };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed) || !Array.isArray((parsed as { entries: unknown }).entries)) {\n throw new Error(`notifier: malformed history.json at ${filePath}`);\n }\n return parsed as NotifierHistoryFile;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free. Shared by\n// `engine.publish` (throws on error) and the host's HTTP route\n// (returns 400 on error). Single source of truth so plugin-runtime\n// callers and HTTP callers can't drift.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport type NotifierLogger = Pick<MinimalLogger, \"warn\" | \"error\">;\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): MutationResult[] {\n return batch.map((waiter) => {\n try {\n return { ok: true, outcome: waiter.mutate(state) };\n } catch (err) {\n return { ok: false, error: err };\n }\n });\n}\n\nfunction collectEvents(results: MutationResult[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(results: MutationResult[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(batch: Waiter[], results: MutationResult[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (let index = 0; index < batch.length; index += 1) {\n const result = results[index];\n if (result.ok) batch[index].resolve();\n else batch[index].reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const results = applyBatchMutations(batch, state);\n const events = collectEvents(results);\n const historyEntries = collectHistoryEntries(results);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(batch, results);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary so plugin-runtime callers and\n // HTTP callers hit the same wall.\n const validationError = validatePublishInput(input as PublishInput);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n lifecycle: input.lifecycle,\n title: input.title,\n body: input.body,\n navigateTarget: input.navigateTarget,\n pluginData: input.pluginData,\n createdAt: new Date().toISOString(),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry as NotifierEntry;\n return { event: { type: \"published\", entry: entry as NotifierEntry } };\n });\n return { id: entryId };\n}\n\n/** Remove an active entry and record its terminal history. `clear`\n * (user dismissed) and `cancel` (producer withdrew) differ only in the\n * emitted event / history reason. No-op when the id is unknown. */\nasync function terminateEntry(entryId: string, reason: \"cleared\" | \"cancelled\"): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: reason === \"cleared\" ? { type: \"cleared\", id: entryId } : { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, reason),\n };\n });\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cleared\");\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cancelled\");\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"notifier-tMsAXyXp.cjs","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. No deep-link target.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules are mirrored in the HTTP layer so plugin-runtime callers\n * and HTTP callers hit the same wall. */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`). The\n * engine never inspects it — used only for `listFor()` filtering\n * and as a UI grouping key. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, with `&notificationId=<id>` appended\n * so the landing page can identify which entry to clear. The\n * engine doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string;\n lifecycle?: NotifierLifecycle;\n navigateTarget?: string;\n pluginData?: TPluginData;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap. The bell popup's History section renders this\n * many entries; older ones fall off when new terminations land. */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport type { NotifierFile, NotifierHistoryFile } from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nfunction isNotFoundError(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet (first ever call on a fresh workspace). Any other\n * read or parse failure throws — the caller has to decide whether to\n * surface or recover, since silently treating \"malformed file\" as\n * \"no entries\" would lose data. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: {} };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n // `typeof null === \"object\"` and `Array.isArray([])` is also true,\n // so a naive `typeof entries !== \"object\"` check would let\n // `{ entries: null }` and `{ entries: [] }` through, which then\n // crash downstream `engine.get` / `list*` mutations. Reject both\n // shapes here at load time so the failure surfaces as a clear\n // \"malformed file\" error.\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n const { entries } = parsed as { entries: unknown };\n if (typeof entries !== \"object\" || entries === null || Array.isArray(entries)) {\n throw new Error(`notifier: malformed active.json at ${filePath}`);\n }\n return parsed as NotifierFile;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n let text: string;\n try {\n text = await fsPromises.readFile(filePath, \"utf-8\");\n } catch (err) {\n if (isNotFoundError(err)) return { entries: [] };\n throw err;\n }\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed) || !Array.isArray((parsed as { entries: unknown }).entries)) {\n throw new Error(`notifier: malformed history.json at ${filePath}`);\n }\n return parsed as NotifierHistoryFile;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free. Shared by\n// `engine.publish` (throws on error) and the host's HTTP route\n// (returns 400 on error). Single source of truth so plugin-runtime\n// callers and HTTP callers can't drift.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport type NotifierLogger = Pick<MinimalLogger, \"warn\" | \"error\">;\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): MutationResult[] {\n return batch.map((waiter) => {\n try {\n return { ok: true, outcome: waiter.mutate(state) };\n } catch (err) {\n return { ok: false, error: err };\n }\n });\n}\n\nfunction collectEvents(results: MutationResult[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(results: MutationResult[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const result of results) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(batch: Waiter[], results: MutationResult[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (let index = 0; index < batch.length; index += 1) {\n const result = results[index];\n if (result.ok) batch[index].resolve();\n else batch[index].reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const results = applyBatchMutations(batch, state);\n const events = collectEvents(results);\n const historyEntries = collectHistoryEntries(results);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(batch, results);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary so plugin-runtime callers and\n // HTTP callers hit the same wall.\n const validationError = validatePublishInput(input as PublishInput);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n lifecycle: input.lifecycle,\n title: input.title,\n body: input.body,\n navigateTarget: input.navigateTarget,\n pluginData: input.pluginData,\n createdAt: new Date().toISOString(),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry as NotifierEntry;\n return { event: { type: \"published\", entry: entry as NotifierEntry } };\n });\n return { id: entryId };\n}\n\n/** Remove an active entry and record its terminal history. `clear`\n * (user dismissed) and `cancel` (producer withdrew) differ only in the\n * emitted event / history reason. No-op when the id is unknown. */\nasync function terminateEntry(entryId: string, reason: \"cleared\" | \"cancelled\"): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: reason === \"cleared\" ? { type: \"cleared\", id: entryId } : { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, reason),\n };\n });\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cleared\");\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cancelled\");\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}
@@ -1,17 +0,0 @@
1
- //#region src/collection/core/project.ts
2
- /** Keep only `fields` (+ `primaryKey`, always) on each record. No `fields`
3
- * ⇒ records pass through untouched. */
4
- function projectRecordFields(items, fields, primaryKey) {
5
- if (!fields) return items;
6
- const keep = /* @__PURE__ */ new Set([primaryKey, ...fields]);
7
- return items.map((item) => Object.fromEntries(Object.entries(item).filter(([key]) => keep.has(key))));
8
- }
9
- //#endregion
10
- Object.defineProperty(exports, "projectRecordFields", {
11
- enumerable: true,
12
- get: function() {
13
- return projectRecordFields;
14
- }
15
- });
16
-
17
- //# sourceMappingURL=project-BWI5w_BT.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-BWI5w_BT.cjs","names":[],"sources":["../src/collection/core/project.ts"],"sourcesContent":["// Field projection over records — the ONE implementation behind both the\n// server store layer (`server/storePage.ts#projectItemFields`) and the\n// remote-view page builder (`remote-view#projectItems`). Isomorphic and\n// dependency-free so either side can import it; extracted to kill the\n// jscpd duplicate the two copies were.\n\n/** Keep only `fields` (+ `primaryKey`, always) on each record. No `fields`\n * ⇒ records pass through untouched. */\nexport function projectRecordFields<T extends Record<string, unknown>>(items: T[], fields: readonly string[] | undefined, primaryKey: string): T[] {\n if (!fields) return items;\n const keep = new Set([primaryKey, ...fields]);\n return items.map((item) => Object.fromEntries(Object.entries(item).filter(([key]) => keep.has(key))) as T);\n}\n"],"mappings":";;;AAQA,SAAgB,oBAAuD,OAAY,QAAuC,YAAyB;CACjJ,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,uBAAO,IAAI,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;CAC5C,OAAO,MAAM,KAAK,SAAS,OAAO,YAAY,OAAO,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,SAAS,KAAK,IAAI,GAAG,CAAC,CAAC,CAAM;AAC3G"}
@@ -1,12 +0,0 @@
1
- //#region src/collection/core/project.ts
2
- /** Keep only `fields` (+ `primaryKey`, always) on each record. No `fields`
3
- * ⇒ records pass through untouched. */
4
- function projectRecordFields(items, fields, primaryKey) {
5
- if (!fields) return items;
6
- const keep = /* @__PURE__ */ new Set([primaryKey, ...fields]);
7
- return items.map((item) => Object.fromEntries(Object.entries(item).filter(([key]) => keep.has(key))));
8
- }
9
- //#endregion
10
- export { projectRecordFields as t };
11
-
12
- //# sourceMappingURL=project-bU98ycsy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-bU98ycsy.js","names":[],"sources":["../src/collection/core/project.ts"],"sourcesContent":["// Field projection over records — the ONE implementation behind both the\n// server store layer (`server/storePage.ts#projectItemFields`) and the\n// remote-view page builder (`remote-view#projectItems`). Isomorphic and\n// dependency-free so either side can import it; extracted to kill the\n// jscpd duplicate the two copies were.\n\n/** Keep only `fields` (+ `primaryKey`, always) on each record. No `fields`\n * ⇒ records pass through untouched. */\nexport function projectRecordFields<T extends Record<string, unknown>>(items: T[], fields: readonly string[] | undefined, primaryKey: string): T[] {\n if (!fields) return items;\n const keep = new Set([primaryKey, ...fields]);\n return items.map((item) => Object.fromEntries(Object.entries(item).filter(([key]) => keep.has(key))) as T);\n}\n"],"mappings":";;;AAQA,SAAgB,oBAAuD,OAAY,QAAuC,YAAyB;CACjJ,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,uBAAO,IAAI,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;CAC5C,OAAO,MAAM,KAAK,SAAS,OAAO,YAAY,OAAO,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,SAAS,KAAK,IAAI,GAAG,CAAC,CAAC,CAAM;AAC3G"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"promptSafety-C--op_6a.js","names":[],"sources":["../src/collection/core/actionVisible.ts","../src/collection/core/backlinks.ts","../src/collection/core/linkTargets.ts","../src/collection/core/where.ts","../src/collection/core/completion.ts","../src/collection/core/dynamicIcon.ts","../src/collection/core/derivedFormula.ts","../src/collection/core/ownProp.ts","../src/collection/core/deriveAll.ts","../src/collection/core/promptSafety.ts"],"sourcesContent":["// 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\nimport { fieldTextOrNull } from \"./fieldText\";\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 // `fieldTextOrNull` rather than `String(...)`: an array/object field would\n // stringify to \"[object Object]\" and could match a `when.in` entry by\n // accident. No text ⇒ no match, same as an absent field.\n const text = fieldTextOrNull(record[when.field]);\n if (text === null) return false;\n return when.in.includes(text);\n}\n\n/** Minimal shape this helper needs from an action — the optional state\n * gate, whichever name its kind uses: `when` on the seeded kinds\n * (chat/agent), `require` on mutate. Accepts the full CollectionAction\n * union (each variant declares at most one of the two). */\nexport interface ActionWithWhen {\n when?: WhenPredicate;\n require?: WhenPredicate;\n}\n\n/** True when the action's button should render against `record` — and,\n * server-side, whether it may RUN (visibility is the authorization\n * rule, for every kind). */\nexport function actionVisible(action: ActionWithWhen, record: Record<string, unknown>): boolean {\n return whenMatches(action.when ?? action.require, record);\n}\n\n/** The run key naming one in-flight `kind: \"agent\"` action button —\n * written by the server's dispatch guard, read back by the client from\n * the detail response's `runningActions` to drive the spinner. ONE\n * builder (isomorphic) so the two sides can't drift. Collection-level\n * and per-record actions live in distinct namespaces so an id collision\n * between `actions` and `collectionActions` can't alias. */\nexport function agentActionRunKey(actionId: string, itemId?: string): string {\n return itemId === undefined ? `collection/${actionId}` : `item/${itemId}/${actionId}`;\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 resolution for `backlinks` fields (plan step ② of\n// plans/done/collection-ontology.md): the display-only reverse side of `ref`.\n// Both the server enrichment (`server/derive.ts`) and the client detail\n// view derive the row set through THESE helpers, so the LLM (getItems)\n// and the user (record panel) always see the same rows — the same\n// single-implementation rule `deriveAll` follows for formulas. No zod,\n// no I/O; safe for the browser barrel.\n\nimport { whenMatches } from \"./actionVisible\";\nimport { fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionFieldSpec, CollectionItem } from \"./schema\";\n\n/** The `backlinks` member of the field-spec union. */\nexport type BacklinksFieldSpec = Extract<CollectionFieldSpec, { type: \"backlinks\" }>;\n\n/** Does `item` reference `recordId` through `via`? Three shapes, tried in\n * order so the dotted form never regresses an existing flat key:\n *\n * - Exact key (`via` is an own property of `item`): the field holds the id\n * directly — compared as text, like every ref deref. This is checked FIRST\n * because `schemaZ` accepts any field name (`z.string()`), so a field\n * literally named `\"client.id\"` keeps its pre-nesting flat-match behaviour\n * instead of being reinterpreted as a table path. A field holding an\n * array/object has no id to compare, so it matches nothing (rather than\n * testing \"[object Object]\" against the record id).\n * - Nested ref (`via: \"<tableField>.<refColumn>\"`, split on the FIRST `.`,\n * only when no exact key exists): the source stores its ref one level down,\n * inside a `table` field's rows (the ontology scanner advertises these as\n * `${key}.${subKey}`). Matches when `item[tableField]` is an array and ANY\n * row's `refColumn` derefs to `recordId`. A record referencing the target in\n * several rows still matches once — the caller filters over source records,\n * each yielded at most once.\n * - No dot and no such key: matches nothing.\n *\n * Fail-soft throughout: a dotted `via` whose table field is absent / non-array,\n * or whose rows lack the column, matches nothing — the same \"a `via` that\n * doesn't resolve matches nothing\" contract as the flat case. Deeper nesting\n * (`a.b.c`) makes `b.c` the column name, which no row carries → no match. */\nexport function viaMatches(via: string, item: CollectionItem, recordId: string): boolean {\n if (Object.hasOwn(item, via)) return fieldTextOrNull(item[via]) === recordId;\n const dot = via.indexOf(\".\");\n if (dot === -1) return false;\n const tableField = via.slice(0, dot);\n const refColumn = via.slice(dot + 1);\n const rows = item[tableField];\n if (!Array.isArray(rows)) return false;\n return rows.some((row) => fieldTextOrNull((row as CollectionItem)?.[refColumn]) === recordId);\n}\n\n/** The SOURCE records whose `via` field stores `recordId` (compared as\n * strings, like every ref deref), with the optional `filter` applied —\n * in the source items' given order. `via` may be a top-level column or a\n * `<tableField>.<refColumn>` path into a `table` field's rows (see\n * {@link viaMatches}). Fail-soft by construction: a `via` that doesn't\n * resolve on the source records simply matches nothing.\n * Callers pass DERIVED source records, so a `filter`/`display` on a\n * derived column works when its formula is SELF-CONTAINED (an invoice\n * `total` = sum over its own line items); a source column that derefs\n * yet another collection stays absent — the same each-record-derives-\n * against-itself rule ref targets follow. */\nexport function backlinkRows(spec: Pick<BacklinksFieldSpec, \"via\" | \"filter\">, recordId: string, sourceItems: CollectionItem[]): CollectionItem[] {\n if (!recordId) return [];\n return sourceItems.filter((item) => viaMatches(spec.via, item, recordId) && whenMatches(spec.filter, item));\n}\n\n/** Project one backlink row to the keys consumers surface: the source\n * collection's primaryKey (rows must stay addressable — it's the link\n * target) plus the declared `display` columns. Keys the row doesn't\n * carry are simply absent, mirroring `projectFields` in getItems. */\nexport function projectBacklinkRow(row: CollectionItem, display: readonly string[], primaryKey: string): CollectionItem {\n const keys = display.includes(primaryKey) ? display : [primaryKey, ...display];\n // Own-property only (matches `viaMatches` above): a display column named\n // `toString` must be absent, not project an inherited prototype function.\n return Object.fromEntries(keys.filter((key) => Object.hasOwn(row, key)).map((key) => [key, row[key]]));\n}\n\n/** The `rollup` member of the field-spec union. */\nexport type RollupFieldSpec = Extract<CollectionFieldSpec, { type: \"rollup\" }>;\n\n/** Numeric coercion shared by the strict record lint (`./recordZ`) and\n * rollup sums: a plain number, or a non-blank numeric string (renderers\n * coerce those via `Number(...)`, so they display fine). Anything else —\n * arrays (`[]` stringifies to `\"\"` = 0, `[42]` to `\"42\"`), booleans,\n * objects — is NaN. Lives here (zod-free) so both consumers share one\n * definition of \"numeric\". */\nexport function coerceNumeric(value: unknown): number {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\" && value.trim() !== \"\") return Number(value);\n return NaN;\n}\n\n/** The rollup aggregate over the matching source rows (plan step ⑤):\n * `count` = how many rows match; `sum` = the total of `column` over\n * them, skipping non-numeric / absent values (a partially-filled source\n * still sums what's there). An EMPTY match set is a real 0 — the\n * fail-soft null lives at the caller, for a source collection that\n * couldn't be resolved at all. Same derived-source-records contract as\n * `backlinkRows`: pass records derived against themselves, so summing a\n * self-contained derived column (an invoice `total`) works. */\nexport function rollupValue(spec: Pick<RollupFieldSpec, \"via\" | \"filter\" | \"op\" | \"column\">, recordId: string, sourceItems: CollectionItem[]): number {\n const rows = backlinkRows(spec, recordId, sourceItems);\n if (spec.op === \"count\") return rows.length;\n let total = 0;\n for (const row of rows) {\n const value = coerceNumeric(spec.column === undefined ? undefined : row[spec.column]);\n if (Number.isFinite(value)) total += value;\n }\n return total;\n}\n","// The single implementation of \"which linked collections must be\n// preloaded\" for a schema — walked identically by the server (derive /\n// enrich, `server/derive.ts`) and the client (linked-cache fetch,\n// collection-plugin). Both sides MUST agree: the server derefs exactly the\n// collections the client caches, so a new ref-shaped field type is taught\n// here once instead of drifting between two mirrored copies. Pure schema\n// walk, no zod / I/O — safe for the browser barrel.\n\nimport type { CollectionFieldSpec, CollectionSchema } from \"./schema\";\n\n/** Slugs of every collection referenced by a `ref` field — top-level and\n * one level into `table` sub-fields (nested tables are schema-rejected,\n * so a single recursion suffices). */\nexport function uniqueRefTargets(schema: CollectionSchema): string[] {\n const targets = new Set<string>();\n const walk = (fields: Record<string, CollectionFieldSpec>): void => {\n for (const field of Object.values(fields)) {\n if (field.type === \"ref\" && typeof field.to === \"string\" && field.to.length > 0) targets.add(field.to);\n if (field.type === \"table\" && field.of) walk(field.of);\n }\n };\n walk(schema.fields);\n return [...targets];\n}\n\n/** Slugs of every collection referenced by an `embed` field. Top-level\n * only — the schema rejects `embed` inside a table's `of`, so no\n * recursion. */\nexport function uniqueEmbedTargets(schema: CollectionSchema): string[] {\n const targets = new Set<string>();\n for (const field of Object.values(schema.fields)) {\n if (field.type === \"embed\" && typeof field.to === \"string\" && field.to.length > 0) targets.add(field.to);\n }\n return [...targets];\n}\n\n/** Slugs of every SOURCE collection a `backlinks` or `rollup` field\n * reverses over — loaded once (the two field kinds share one load).\n * Top-level only, like `embed` (the schema rejects both inside a table's\n * `of`). */\nexport function uniqueBacklinkSources(schema: CollectionSchema): string[] {\n const sources = new Set<string>();\n for (const field of Object.values(schema.fields)) {\n if ((field.type === \"backlinks\" || field.type === \"rollup\") && field.from.length > 0) sources.add(field.from);\n }\n return [...sources];\n}\n","// Pure SQL-like `where` predicate for `dynamicIcon` (see\n// `DynamicIconSource.where` / `DynamicIconRule.where` in `./schema`).\n// An AND of typed conditions — richer than the single-field `CollectionWhen`\n// used elsewhere (fields/actions via `./actionVisible`), which stays as-is\n// for its existing callers. No fs, no host state. The condition SHAPES are\n// derived from the zod source of truth in `./schemaZ` (type-only imports —\n// this evaluator stays zod-free at runtime).\n\nimport type { z } from \"zod\";\nimport type { ValueRefZ, WhereCondZ, WhereZ } from \"./schemaZ\";\n\n/** Reads the comparison value from a field instead of a schema literal:\n * - `record` set → another record: `recordsById[record][field]` (e.g. a\n * `_config` singleton's `defaultCity`, following a per-user setting);\n * - `record` omitted → the SAME record being matched (field-to-field, e.g.\n * `spent > budget`). */\nexport type ValueRef = z.infer<typeof ValueRefZ>;\n\n/** One typed condition: `record[field] <op> value`. The comparison value is\n * either a literal `value` (a plain string for every op except `in`, which\n * takes the allowed set) or a `valueFrom` reference resolved against the\n * `recordsById` map passed to `matchesWhere`. Exactly one of the two is\n * expected — enforced by zod at the schema boundary (`./schemaZ`), not\n * here. */\nexport type WhereCond = z.infer<typeof WhereCondZ>;\n\n/** Comparison operators one `WhereCond` may apply to `record[field]`. */\nexport type WhereOp = WhereCond[\"op\"];\n\n/** A `where` clause is the AND of its conditions — every one must match. */\nexport type Where = z.infer<typeof WhereZ>;\n\n/** True when `record[field]` is absent (`undefined`/`null`) — the only case\n * where `ne` and every other op disagree on the result. */\nfunction isMissing(raw: unknown): boolean {\n return raw === undefined || raw === null;\n}\n\n/** Own-property read for a user/LLM-controlled key (`cond.field`,\n * `valueFrom.record`, `valueFrom.field`). A bare `obj[key]` reaches inherited\n * Object.prototype members, so `field: \"toString\"` would read a function\n * instead of an absent value (spurious match), and `record: \"constructor\"`\n * would read the `Object` function whose `.name`/`.length` are plausible\n * bogus comparands — breaking the \"unresolved ⇒ never matches\" contract\n * (#2323). A prototype key resolves to `undefined` (absent). */\nfunction ownProp<T>(obj: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(obj, key) ? obj[key] : undefined;\n}\n\n/** The effective comparison value for `cond`: its literal `value`, or — for\n * a `valueFrom` reference — the target field read out of `recordsById`.\n * `undefined` means UNRESOLVED (no such record, or the field on it is\n * missing); the caller must treat that as \"never matches\", not as a\n * literal `undefined` value to compare against. */\nfunction resolveValue(cond: WhereCond, record: Record<string, unknown>, recordsById: Record<string, Record<string, unknown>>): string | string[] | undefined {\n if (!cond.valueFrom) return cond.value;\n const { record: refRecord, field } = cond.valueFrom;\n const target = refRecord === undefined ? record : ownProp(recordsById, refRecord);\n const raw = target === undefined ? undefined : ownProp(target, field);\n return isMissing(raw) ? undefined : String(raw);\n}\n\nfunction matchesNumericOp(operator: \"gt\" | \"gte\" | \"lt\" | \"lte\", left: number, right: number): boolean {\n if (operator === \"gt\") return left > right;\n if (operator === \"gte\") return left >= right;\n if (operator === \"lt\") return left < right;\n return left <= right;\n}\n\n/** `Number(\"\")` / `Number(\" \")` are `0`, not `NaN`, so treat a blank string\n * as non-numeric explicitly — an empty field must fail a numeric compare,\n * not read as zero. */\nfunction toNumber(raw: string): number {\n return raw.trim() === \"\" ? NaN : Number(raw);\n}\n\nfunction matchesNumeric(operator: \"gt\" | \"gte\" | \"lt\" | \"lte\", raw: string, value: string | string[]): boolean {\n if (Array.isArray(value)) return false;\n const left = toNumber(raw);\n const right = toNumber(value);\n if (Number.isNaN(left) || Number.isNaN(right)) return false;\n return matchesNumericOp(operator, left, right);\n}\n\n/** True when the present string `raw` satisfies `operator` against the\n * resolved `value` (field known to exist — MISSING is handled by the\n * caller before this runs, and an UNRESOLVED `valueFrom` never reaches\n * here either). */\nfunction matchesPresent(operator: WhereOp, raw: string, value: string | string[]): boolean {\n switch (operator) {\n case \"eq\":\n return raw === String(value);\n case \"ne\":\n return raw !== String(value);\n case \"in\":\n return Array.isArray(value) && value.includes(raw);\n case \"contains\":\n return raw.includes(String(value));\n case \"gt\":\n case \"gte\":\n case \"lt\":\n case \"lte\":\n return matchesNumeric(operator, raw, value);\n default:\n return false;\n }\n}\n\n/** True when `record` satisfies one condition, given `recordsById` to\n * resolve a `valueFrom` reference. Two independent MISSING cases, checked\n * in order:\n * - `record[cond.field]` absent (`undefined`/`null`) → matches only `ne`\n * (vacuously true — \"not equal to X\" holds when there's no value at\n * all); every other op is false. Unchanged from the literal-`value`\n * behaviour, regardless of whether `valueFrom` would also resolve.\n * - the resolved comparison value is UNRESOLVED (a `valueFrom` whose\n * target record/field doesn't exist) → false for EVERY op, including\n * `ne` — a broken reference must never spuriously match. */\nfunction matchesCond(cond: WhereCond, record: Record<string, unknown>, recordsById: Record<string, Record<string, unknown>>): boolean {\n const raw = ownProp(record, cond.field);\n if (isMissing(raw)) return cond.op === \"ne\";\n const value = resolveValue(cond, record, recordsById);\n if (value === undefined) return false;\n return matchesPresent(cond.op, String(raw), value);\n}\n\n/** True when `record` satisfies every condition in `where` (AND). An empty\n * `where` matches everything. `recordsById` — the source collection's\n * records keyed by primaryKey — resolves any `valueFrom` reference;\n * omitted (default `{}`) for callers with no cross-record lookups, in\n * which case every `valueFrom` condition is UNRESOLVED and so never\n * matches. */\nexport function matchesWhere(where: Where, record: Record<string, unknown>, recordsById: Record<string, Record<string, unknown>> = {}): boolean {\n return where.every((cond) => matchesCond(cond, record, recordsById));\n}\n","// The \"is this record done?\" predicate — THE single implementation,\n// shared by the notification reconciler (bell clearing, collection-watchers),\n// spawn (successor-predicate fallback, ../server/spawn.ts), and view-side\n// completion filters. Zod-free and I/O-free like the rest of `core/`, so\n// it is browser-safe through the collection barrel.\n//\n// Two completion forms (see `CollectionSchemaZ`'s completion refine):\n// - legacy pair: `completionField` names a stored field and\n// `completionDoneValues` lists the values that mean done —\n// done ⇔ `String(item[completionField])` ∈ `completionDoneValues`.\n// - flag form: `completionField` names a `flag` field (and\n// `completionDoneValues` is absent) — done ⇔ the flag's `where`\n// matches. Evaluated directly against the raw record here (NOT read\n// from a materialized value) because callers like the reconciler and\n// spawn work on records straight off disk, before any `deriveAll`\n// enrichment. That raw evaluation is CORRECT BY CONSTRUCTION: a\n// schema-level refine rejects a completion flag whose `where`\n// references computed fields, so every condition reads stored data.\n\nimport { fieldText, fieldTextOrNull } from \"./fieldText\";\nimport { matchesWhere, type Where } from \"./where\";\nimport type { CollectionFieldSpec, CollectionItem } from \"./schema\";\n\n/** The slice of a parsed schema the done predicate reads — minimal\n * structural shape (like `DerivableSchema`) so the client and server\n * `CollectionSchema` types both satisfy it as-is. */\nexport interface CompletionSchemaView {\n /** Optional so legacy-pair callers (and their test fixtures) that\n * never consult field specs keep working; only the flag form needs\n * to look the completion field up. */\n fields?: Record<string, { type: string; where?: Where }>;\n completionField?: string;\n completionDoneValues?: readonly string[];\n}\n\n/** True iff the schema declares completion tracking AND `item` is done\n * under whichever completion form the schema uses (see module doc). */\nexport function itemIsDone(schema: CompletionSchemaView, item: Record<string, unknown>): boolean {\n const { completionField, completionDoneValues } = schema;\n if (!completionField) return false;\n const spec = schema.fields?.[completionField];\n if (spec?.type === \"flag\" && spec.where) return matchesWhere(spec.where, item);\n if (!completionDoneValues) return false;\n // An array/object field has no text form; treat it as \"not done\" rather than\n // letting \"[object Object]\" match a configured done-value.\n const text = fieldTextOrNull(item[completionField]);\n if (text === null) return false;\n return completionDoneValues.includes(text);\n}\n\n/** The schema slice `completionCoveredByFieldChip` reads: field kinds (it\n * inspects `boolean` / `toggle` variants' `field` / `onValue`) plus the\n * completion pair. Minimal structural shape so both the client and server\n * `CollectionSchema` types satisfy it as-is. */\nexport interface CompletionChipSchemaView {\n fields: Record<string, { type: string; field?: string; onValue?: string }>;\n completionField?: string;\n completionDoneValues?: readonly string[];\n}\n\n/** True when an existing FIELD chip already expresses the legacy completion\n * predicate exactly, so a synthesized \"done\" chip would be a duplicate: a\n * boolean `completionField` (done ⇔ `\"true\"` ⇔ the boolean's own chip), or a\n * `toggle` projecting the `completionField` whose `onValue` is the single\n * done value (the todos-schema shape: toggle \"Done\" on `status` +\n * `completionDoneValues: [\"done\"]`). A superset pair (extra done values)\n * still synthesizes — no field chip covers it. */\nexport function completionCoveredByFieldChip(schema: CompletionChipSchemaView): boolean {\n const { completionField, completionDoneValues } = schema;\n if (completionDoneValues?.length !== 1) return false;\n const [doneValue] = completionDoneValues;\n if (schema.fields[completionField ?? \"\"]?.type === \"boolean\") return doneValue === \"true\";\n return Object.values(schema.fields).some((field) => field.type === \"toggle\" && field.field === completionField && field.onValue === doneValue);\n}\n\n/** Whether a `toggle` field reads as checked: its projected enum field currently\n * equals `onValue`. The toggle stores nothing of its own, so this reads the raw\n * projected value; a non-toggle field is never checked. */\nexport function toggleChecked(item: CollectionItem, field: CollectionFieldSpec): boolean {\n return field.type === \"toggle\" && fieldText(item[field.field]) === field.onValue;\n}\n\n/** A `flag` FIELD's boolean for one row, read off the already-enriched record\n * (so a flag computed from derived/rollup inputs is correct). Strict `=== true`\n * on purpose: a truthy non-boolean (\"yes\", 1) is NOT a set flag, so a stray\n * value can never render as an active flag. */\nexport function flagFieldValue(record: Record<string, unknown>, key: string): boolean {\n return record[key] === true;\n}\n\n/** One entry in the table's flag-filter menu: a real `flag` / `boolean` /\n * `toggle` field, or the synthesized legacy-completion chip (`synthetic`,\n * predicate = `itemIsDone`). */\nexport interface FlagChip {\n key: string;\n label: string;\n synthetic?: boolean;\n}\n\n/** The schema slice `chipMatches` reads: full field specs (it inspects the\n * `toggle` variant's `field` / `onValue`) plus the completion pair that\n * `itemIsDone` needs for the synthesized chip. */\nexport interface ChipMatchSchema {\n fields: Record<string, CollectionFieldSpec>;\n completionField?: string;\n completionDoneValues?: readonly string[];\n}\n\n/** Whether one row satisfies a chip's predicate: `itemIsDone` for the\n * synthesized completion chip, the projected value for a `toggle`, the stored\n * boolean for a `boolean`, else the computed flag value. The flag branch reads\n * the ENRICHED record via the injected `deriveRecord` (kept a parameter so this\n * stays pure and framework-free). */\nexport function chipMatches(\n chip: FlagChip,\n schema: ChipMatchSchema,\n item: CollectionItem,\n deriveRecord: (item: CollectionItem) => Record<string, unknown>,\n): boolean {\n if (chip.synthetic) return itemIsDone(schema, item);\n const field = schema.fields[chip.key];\n if (field?.type === \"toggle\") return toggleChecked(item, field);\n if (field?.type === \"boolean\") return item[chip.key] === true;\n return flagFieldValue(deriveRecord(item), chip.key);\n}\n","// Pure resolver for a collection's dynamic launcher-shortcut icon (see\n// `CollectionSchema.dynamicIcon`). Selects one \"source\" record from a\n// (possibly cross-collection, optionally `where`-filtered) records pool,\n// then maps it through a first-match-wins rules list to an icon name.\n// No fs, no host state — the server-side compute\n// (`packages/core/src/collection/server/dynamicIcon.ts`) loads the source\n// collection's records and calls these.\n\nimport { fieldText } from \"./fieldText\";\nimport { matchesWhere } from \"./where\";\nimport type { CollectionFieldSpec, CollectionItem, CollectionSchema, DynamicIconSource, DynamicIconSpec } from \"./schema\";\n\n/** The record with the greatest `String(record[field])` (localeCompare) —\n * ties keep the first-seen record (stable left-to-right `reduce`). */\nfunction latestByField(pool: CollectionItem[], field: string): CollectionItem {\n return pool.reduce((latest, candidate) => (fieldText(candidate[field]).localeCompare(fieldText(latest[field])) > 0 ? candidate : latest));\n}\n\n/** Reduce `records` to the one record that decides the effective icon, per\n * `source`'s `where` filter + `from` strategy:\n * - pool = `source.where`-filtered records, or every record when unset;\n * - an empty pool resolves to `null` (no source record → fallback);\n * - `from: \"first\"` / `\"when\"` → the first pool record (storage order);\n * - `from: \"latest\"` (default), with `orderBy` given → the pool record\n * whose `String(record[orderBy])` sorts highest;\n * - `from: \"latest\"`, with no `orderBy` → the last pool record.\n * `recordsById` (the source collection's records keyed by primaryKey)\n * resolves any `valueFrom` reference inside `source.where`; omitted for\n * callers with no cross-record lookups. */\nexport function selectDynamicRecord(\n records: CollectionItem[],\n source: DynamicIconSource,\n orderBy: string | undefined,\n recordsById: Record<string, CollectionItem> = {},\n): CollectionItem | null {\n const { where } = source;\n const pool = where ? records.filter((record) => matchesWhere(where, record, recordsById)) : records;\n if (pool.length === 0) return null;\n if (source.from === \"first\" || source.from === \"when\") return pool[0];\n return orderBy ? latestByField(pool, orderBy) : pool[pool.length - 1];\n}\n\n/** Map a resolved source record to the effective icon: `spec.fallback`\n * (or the collection's own static `icon`) when there's no record or no\n * rule matches; otherwise the `icon` of the first rule whose `where`\n * matches the record. `recordsById` resolves any `valueFrom` reference\n * inside a rule's `where`, same as `selectDynamicRecord`. */\nexport function resolveIcon(\n record: CollectionItem | null,\n spec: DynamicIconSpec,\n staticIcon: string,\n recordsById: Record<string, CollectionItem> = {},\n): string {\n const fallback = spec.fallback ?? staticIcon;\n if (!record) return fallback;\n const matched = spec.rules.find((rule) => matchesWhere(rule.where, record, recordsById));\n return matched ? matched.icon : fallback;\n}\n\nconst isDateLikeField = (field: CollectionFieldSpec): boolean => field.type === \"date\" || field.type === \"datetime\";\n\n/** The first field key (declaration order) whose type is `date` or\n * `datetime` — the default `orderBy` for `from: \"latest\"` when a\n * `DynamicIconSource` doesn't name one. `undefined` when the schema has\n * no date-like field. */\nexport function firstDateField(schema: CollectionSchema): string | undefined {\n return Object.entries(schema.fields).find(([, field]) => isDateLikeField(field))?.[0];\n}\n","// Tiny expression evaluator for the `derived` field type on\n// schema-driven collections (see plans/done/feat-mc-invoice.md).\n//\n// Grammar (recursive-descent, no precedence climbing — six\n// non-terminals total):\n//\n// expr := term (('+' | '-') term)*\n// term := factor (('*' | '/') factor)*\n// factor := number | sumCall | refAccess | identifier | '(' expr ')'\n// sumCall:= 'sum' '(' sumArg ')'\n// sumArg := tableCol (('*' | '/') tableCol)* // e.g. lineItems[].quantity * lineItems[].rate\n// tableCol := identifier '[]' '.' identifier\n// refAccess := identifier '.' identifier // e.g. ticker.price — deref a ref field into its target record\n//\n// `identifier` accepts top-level field names (single segment).\n// Inside `sumArg`, identifiers are the `<table>[].col` form.\n// A two-segment `<field>.<col>` at factor level is a *ref deref*:\n// `<field>` must be a `ref`-typed field on this record (its stored\n// value is the target item's slug), and `<col>` is a numeric column\n// read from that target record. The caller resolves the target into\n// `ctx.refs` (it owns the schema + the loaded target collection);\n// the evaluator stays pure and never does I/O.\n//\n// What's deliberately NOT supported (and would parse-error rather\n// than silently misbehave):\n// - String literals, boolean operators, comparisons, conditionals\n// - Nested function calls beyond `sum(...)`\n// - Anything in the record that isn't a number / table-of-objects\n//\n// All evaluation is pure — no eval(), no Function constructor.\n// Returns `null` on any failure (parse error, unbound identifier,\n// non-finite arithmetic). The caller renders `null` as em-dash in\n// the table cell + form display.\n\nexport interface FormulaContext {\n /** The record being evaluated. For derived fields in the form,\n * this is the live draft (text + table both converted via the\n * same `draftToRecord` pipeline). For the main table cell,\n * this is the persisted item. */\n record: Record<string, unknown>;\n /** Resolved ref-target records for THIS row, keyed by the local\n * `ref` field name. The caller (which has the schema + the linked\n * collection's items loaded) maps each ref field's stored slug to\n * the full target record and passes it here, so a `<field>.<col>`\n * formula can read a numeric column off the referenced record\n * (e.g. `shares * ticker.price`). A missing key or `null` value\n * (unknown field / dangling slug) makes that deref evaluate to\n * NaN → the whole formula returns `null` → em-dash, consistent\n * with every other failure mode. Absent ⇒ no refs available. */\n refs?: Record<string, Record<string, unknown> | null>;\n}\n\nexport function evaluateDerived(formula: string, ctx: FormulaContext): number | null {\n let tokens: Token[];\n try {\n tokens = tokenize(formula);\n } catch {\n return null;\n }\n // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Parser class is defined later in the file (grouped with its AST + evaluator); evaluateDerived runs after module init so the TDZ concern doesn't apply.\n const parser = new Parser(tokens);\n let ast: Node;\n try {\n ast = parser.parseExpr();\n if (!parser.atEnd()) return null; // trailing junk\n } catch {\n return null;\n }\n const value = evaluate(ast, ctx);\n return Number.isFinite(value) ? value : null;\n}\n\n// ─── Tokens ────────────────────────────────────────────────\n\ntype TokenKind = \"number\" | \"ident\" | \"(\" | \")\" | \"+\" | \"-\" | \"*\" | \"/\" | \"[]\" | \".\";\n\ninterface Token {\n kind: TokenKind;\n value?: string | number;\n}\n\nconst SINGLE_CHAR_PUNCT = new Set<TokenKind>([\"(\", \")\", \"+\", \"-\", \"*\", \"/\", \".\"]);\n\ninterface Cursor {\n input: string;\n index: number;\n}\n\nfunction consumeWhitespace(cur: Cursor): boolean {\n const char = cur.input[cur.index];\n if (char === \" \" || char === \"\\t\" || char === \"\\n\") {\n cur.index++;\n return true;\n }\n return false;\n}\n\nfunction consumeNumber(cur: Cursor): Token | null {\n const char = cur.input[cur.index] ?? \"\";\n const next = cur.input[cur.index + 1] ?? \"\";\n if (!isDigit(char) && !(char === \".\" && isDigit(next))) return null;\n let raw = \"\";\n while (cur.index < cur.input.length) {\n const here = cur.input[cur.index] ?? \"\";\n if (!isDigit(here) && here !== \".\") break;\n raw += here;\n cur.index++;\n }\n const num = Number(raw);\n if (!Number.isFinite(num)) throw new Error(\"bad number\");\n return { kind: \"number\", value: num };\n}\n\nfunction consumeIdent(cur: Cursor): Token | null {\n const char = cur.input[cur.index] ?? \"\";\n if (!isIdentStart(char)) return null;\n let raw = \"\";\n while (cur.index < cur.input.length && isIdentChar(cur.input[cur.index] ?? \"\")) {\n raw += cur.input[cur.index];\n cur.index++;\n }\n return { kind: \"ident\", value: raw };\n}\n\nfunction consumePunct(cur: Cursor): Token | null {\n const char = cur.input[cur.index] ?? \"\";\n if (char === \"[\" && cur.input[cur.index + 1] === \"]\") {\n cur.index += 2;\n return { kind: \"[]\" };\n }\n if (SINGLE_CHAR_PUNCT.has(char as TokenKind)) {\n cur.index++;\n return { kind: char as TokenKind };\n }\n return null;\n}\n\nfunction tokenize(input: string): Token[] {\n const tokens: Token[] = [];\n const cur: Cursor = { input, index: 0 };\n while (cur.index < input.length) {\n if (consumeWhitespace(cur)) continue;\n // Number FIRST so a leading-dot literal (`.25`) isn't split by\n // the `.` punctuation branch.\n const numTok = consumeNumber(cur);\n if (numTok) {\n tokens.push(numTok);\n continue;\n }\n const punctTok = consumePunct(cur);\n if (punctTok) {\n tokens.push(punctTok);\n continue;\n }\n const identTok = consumeIdent(cur);\n if (identTok) {\n tokens.push(identTok);\n continue;\n }\n throw new Error(`unexpected char ${input[cur.index]}`);\n }\n return tokens;\n}\n\nfunction isDigit(char: string): boolean {\n return char >= \"0\" && char <= \"9\";\n}\nfunction isIdentStart(char: string): boolean {\n return (char >= \"a\" && char <= \"z\") || (char >= \"A\" && char <= \"Z\") || char === \"_\";\n}\nfunction isIdentChar(char: string): boolean {\n return isIdentStart(char) || isDigit(char);\n}\n\n// ─── AST + Parser ───────────────────────────────────────────\n\ntype Node =\n | { kind: \"num\"; value: number }\n | { kind: \"ident\"; name: string }\n | { kind: \"ref\"; field: string; col: string }\n | { kind: \"binop\"; operator: \"+\" | \"-\" | \"*\" | \"/\"; left: Node; right: Node }\n | { kind: \"sum\"; arg: SumArg };\n\ninterface SumArg {\n // factors multiplied/divided together; each is a (tableName, colName) ref into a row.\n factors: { table: string; col: string }[];\n /** Operators between factors: length = factors.length - 1; each\n * is \"*\" or \"/\". For a single-factor sum (`sum(lineItems[].amount)`)\n * this is empty. */\n operators: (\"*\" | \"/\")[];\n}\n\nclass Parser {\n private cursor = 0;\n constructor(private readonly tokens: Token[]) {}\n\n atEnd(): boolean {\n return this.cursor >= this.tokens.length;\n }\n private peek(): Token | undefined {\n return this.tokens[this.cursor];\n }\n private consume(): Token {\n const tok = this.tokens[this.cursor++];\n if (!tok) throw new Error(\"unexpected end of input\");\n return tok;\n }\n private expect(kind: TokenKind): Token {\n const tok = this.consume();\n if (tok.kind !== kind) throw new Error(`expected ${kind}, got ${tok.kind}`);\n return tok;\n }\n\n parseExpr(): Node {\n let left = this.parseTerm();\n while (this.peek()?.kind === \"+\" || this.peek()?.kind === \"-\") {\n const operator = this.consume().kind as \"+\" | \"-\";\n const right = this.parseTerm();\n left = { kind: \"binop\", operator, left, right };\n }\n return left;\n }\n\n private parseTerm(): Node {\n let left = this.parseFactor();\n while (this.peek()?.kind === \"*\" || this.peek()?.kind === \"/\") {\n const operator = this.consume().kind as \"*\" | \"/\";\n const right = this.parseFactor();\n left = { kind: \"binop\", operator, left, right };\n }\n return left;\n }\n\n private parseFactor(): Node {\n const tok = this.peek();\n if (!tok) throw new Error(\"unexpected end in factor\");\n if (tok.kind === \"number\") {\n this.consume();\n return { kind: \"num\", value: tok.value as number };\n }\n if (tok.kind === \"(\") {\n this.consume();\n const inner = this.parseExpr();\n this.expect(\")\");\n return inner;\n }\n if (tok.kind === \"ident\") {\n const name = (tok.value as string) ?? \"\";\n // sum(...) — only function call we support\n if (name === \"sum\" && this.tokens[this.cursor + 1]?.kind === \"(\") {\n this.consume(); // ident\n this.expect(\"(\");\n const arg = this.parseSumArg();\n this.expect(\")\");\n return { kind: \"sum\", arg };\n }\n this.consume(); // ident\n // ref deref: `<field>.<col>` (e.g. ticker.price). The table-row\n // form `<table>[].col` only appears inside sum(), so a `.`\n // immediately after a top-level ident is unambiguously a ref\n // dereference here.\n if (this.peek()?.kind === \".\") {\n this.consume(); // '.'\n const col = this.expect(\"ident\");\n return { kind: \"ref\", field: name, col: col.value as string };\n }\n return { kind: \"ident\", name };\n }\n throw new Error(`unexpected token ${tok.kind} in factor`);\n }\n\n private parseSumArg(): SumArg {\n const factors: { table: string; col: string }[] = [];\n const operators: (\"*\" | \"/\")[] = [];\n factors.push(this.parseTableCol());\n while (this.peek()?.kind === \"*\" || this.peek()?.kind === \"/\") {\n const operator = this.consume().kind as \"*\" | \"/\";\n operators.push(operator);\n factors.push(this.parseTableCol());\n }\n return { factors, operators };\n }\n\n private parseTableCol(): { table: string; col: string } {\n const tableTok = this.expect(\"ident\");\n this.expect(\"[]\");\n this.expect(\".\");\n const colTok = this.expect(\"ident\");\n return { table: tableTok.value as string, col: colTok.value as string };\n }\n}\n\n// ─── Evaluator ──────────────────────────────────────────────\n\nfunction evaluate(node: Node, ctx: FormulaContext): number {\n if (node.kind === \"num\") return node.value;\n if (node.kind === \"ident\") {\n const raw = ctx.record[node.name];\n return toFiniteNumber(raw);\n }\n if (node.kind === \"ref\") {\n // `<field>.<col>`: read `col` off the resolved target record the\n // caller put in ctx.refs. Unknown field / dangling slug → null →\n // NaN, so the whole formula fails soft to an em-dash.\n const target = ctx.refs?.[node.field] ?? null;\n if (!target) return Number.NaN;\n return toFiniteNumber(target[node.col]);\n }\n if (node.kind === \"binop\") {\n const left = evaluate(node.left, ctx);\n const right = evaluate(node.right, ctx);\n return applyBinop(node.operator, left, right);\n }\n if (node.kind === \"sum\") {\n return evaluateSum(node.arg, ctx);\n }\n // Exhaustive — TS narrows above branches but throw keeps runtime honest.\n throw new Error(`unknown node`);\n}\n\nfunction applyBinop(operator: \"+\" | \"-\" | \"*\" | \"/\", left: number, right: number): number {\n if (!Number.isFinite(left) || !Number.isFinite(right)) return Number.NaN;\n if (operator === \"+\") return left + right;\n if (operator === \"-\") return left - right;\n if (operator === \"*\") return left * right;\n // operator === \"/\"\n if (right === 0) return Number.NaN;\n return left / right;\n}\n\nfunction evaluateSum(arg: SumArg, ctx: FormulaContext): number {\n if (arg.factors.length === 0) return 0;\n const tableName = arg.factors[0].table;\n // All factors must reference the SAME table (you can't multiply\n // a row from lineItems against a row from another table — the\n // semantics would be ambiguous). Reject mismatch.\n for (const factor of arg.factors) {\n if (factor.table !== tableName) return Number.NaN;\n }\n const rows = ctx.record[tableName];\n if (!Array.isArray(rows)) return 0;\n let total = 0;\n for (const row of rows) {\n if (!row || typeof row !== \"object\") continue;\n let product = toFiniteNumber((row as Record<string, unknown>)[arg.factors[0].col]);\n if (!Number.isFinite(product)) return Number.NaN;\n for (let i = 1; i < arg.factors.length; i++) {\n const value = toFiniteNumber((row as Record<string, unknown>)[arg.factors[i].col]);\n if (!Number.isFinite(value)) return Number.NaN;\n product = applyBinop(arg.operators[i - 1], product, value);\n }\n total += product;\n }\n return total;\n}\n\nfunction toFiniteNumber(value: unknown): number {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : Number.NaN;\n if (typeof value === \"string\" && value.length > 0) {\n const num = Number(value);\n return Number.isFinite(num) ? num : Number.NaN;\n }\n return Number.NaN;\n}\n","// Own-property read for a plain object indexed by a user/LLM/feed-controlled\n// key. Extracted so the ref/embed resolution in `deriveAll` and `derive`\n// share one guard (where.ts keeps its own private copy for the dynamicIcon\n// `where` evaluator — #2443).\n\n/** Read `obj[key]` only when it is an OWN property. A bare `obj[key]` reaches\n * inherited Object.prototype members, so a dangling ref/embed id like\n * `\"constructor\"` or `\"__proto__\"` resolves to a prototype value (the\n * `Object` function) instead of being absent — breaking the \"missing target\n * ⇒ null (em-dash)\" fail-soft contract and putting a non-serializable\n * function into the API response, where `JSON.stringify` drops the key\n * entirely (#2322). A prototype key resolves to `undefined`; a record whose\n * id is legitimately `\"constructor\"` (an own key) still resolves. */\nexport function ownProp<T>(obj: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(obj, key) ? obj[key] : undefined;\n}\n","// The derived-field saturation loop for schema-driven collections,\n// extracted from `composables/collections/useCollectionRendering.ts` so\n// the server (manageCollection getItems enrichment) and the client\n// (table cells, form display) evaluate formulas through ONE\n// implementation — if the two ever diverged, the UI and the LLM would\n// disagree on a number. Pure module: no Vue, no I/O.\n//\n// Like `actionVisible.ts`, the input types are minimal structural\n// shapes so both the client `FieldSpec`/`CollectionSchema`\n// (src/components/collectionTypes.ts) and the server\n// `CollectionFieldSpec`/`CollectionSchema`\n// (server/workspace/collections/types.ts) satisfy them as-is.\n\nimport { evaluateDerived, type FormulaContext } from \"./derivedFormula\";\nimport { ownProp } from \"./ownProp\";\nimport { matchesWhere, type Where } from \"./where\";\n\n/** Minimal field shape the derive loop needs — accepts both the client\n * FieldSpec and the server CollectionFieldSpec. */\nexport interface DerivableFieldSpec {\n type: string;\n /** When type === \"ref\": slug of the target collection. */\n to?: string;\n /** When type === \"derived\": formula evaluated against the record. */\n formula?: string;\n /** When type === \"flag\": predicate matched against the record. */\n where?: Where;\n}\n\n/** Minimal schema shape: just the ordered field map. */\nexport interface DerivableSchema {\n fields: Record<string, DerivableFieldSpec>;\n}\n\nexport type DerivableRecord = Record<string, unknown>;\n\n/** Per-target-collection cache of loaded referenced records:\n * target collection slug → item slug → full record. Mirrors the\n * client's `RefRecordCache` / the server's enrichment loader. */\nexport type DeriveRefRecords = Record<string, Record<string, DerivableRecord>>;\n\n/** Map each `ref` field's stored slug to its loaded target record (or\n * null when dangling / not loaded), keyed by the LOCAL field name —\n * the shape `evaluateDerived` reads for `<field>.<col>` derefs. */\nexport function resolveRowRefs(schema: DerivableSchema, record: DerivableRecord, refRecords: DeriveRefRecords): NonNullable<FormulaContext[\"refs\"]> {\n const refs: NonNullable<FormulaContext[\"refs\"]> = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n if (field.type !== \"ref\" || !field.to) continue;\n const slug = record[key];\n const targets = ownProp(refRecords, field.to);\n refs[key] = typeof slug === \"string\" && targets ? (ownProp(targets, slug) ?? null) : null;\n }\n return refs;\n}\n\n/** True for the field types the saturation loop below (re)computes:\n * `derived` formulas and `flag` predicates. */\nfunction isLoopComputed(field: DerivableFieldSpec): boolean {\n return field.type === \"derived\" || field.type === \"flag\";\n}\n\n/** The value one loop-computed field takes against the current\n * `enriched` record: a `derived` formula result (`null` = failed) or a\n * `flag` predicate match (total — always a boolean). `undefined` for\n * every other field type (nothing to compute). */\nfunction computeFieldValue(\n field: DerivableFieldSpec,\n enriched: DerivableRecord,\n refs: NonNullable<FormulaContext[\"refs\"]>,\n): number | boolean | null | undefined {\n if (field.type === \"derived\" && field.formula) return evaluateDerived(field.formula, { record: enriched, refs });\n if (field.type === \"flag\" && field.where) return matchesWhere(field.where, enriched);\n return undefined;\n}\n\n/** Evaluate every `derived` and `flag` field against `base`, saturating\n * so a computed field can read another one computed in an earlier pass\n * (`subtotal → tax → total` converges in ≤ field-count passes; a flag\n * may read a derived value, or another flag via its stringified\n * boolean). Cycles can't loop forever — passes are bounded by the\n * number of computed fields and the loop breaks as soon as a pass\n * changes nothing. Failed formulas stay ABSENT (the UI renders them as\n * em-dash); flags are total (always true/false). Returns a copy;\n * `base` is never mutated.\n *\n * Computed keys already present in `base` are stripped before\n * evaluation: computed output is host-truth, never persisted-input\n * fallback. A record JSON can carry a stale (or forged) computed value\n * — raw Write/Edit, legacy data — and without the strip, a failing\n * formula would silently surface that value as if the host computed\n * it. */\nexport function deriveAll(schema: DerivableSchema, base: DerivableRecord, refRecords: DeriveRefRecords): DerivableRecord {\n const computedKeys = new Set(\n Object.entries(schema.fields)\n .filter(([, field]) => isLoopComputed(field))\n .map(([key]) => key),\n );\n const enriched: DerivableRecord = Object.fromEntries(Object.entries(base).filter(([key]) => !computedKeys.has(key)));\n const refs = resolveRowRefs(schema, base, refRecords);\n for (let pass = 0; pass < computedKeys.size; pass++) {\n let mutated = false;\n for (const [key, field] of Object.entries(schema.fields)) {\n const next = computeFieldValue(field, enriched, refs);\n if (next !== undefined && next !== null && enriched[key] !== next) {\n enriched[key] = next;\n mutated = true;\n }\n }\n if (!mutated) break;\n }\n return enriched;\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 +0,0 @@
1
- {"version":3,"file":"promptSafety-FD8cQn3e.cjs","names":[],"sources":["../src/collection/core/actionVisible.ts","../src/collection/core/backlinks.ts","../src/collection/core/linkTargets.ts","../src/collection/core/where.ts","../src/collection/core/completion.ts","../src/collection/core/dynamicIcon.ts","../src/collection/core/derivedFormula.ts","../src/collection/core/ownProp.ts","../src/collection/core/deriveAll.ts","../src/collection/core/promptSafety.ts"],"sourcesContent":["// 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\nimport { fieldTextOrNull } from \"./fieldText\";\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 // `fieldTextOrNull` rather than `String(...)`: an array/object field would\n // stringify to \"[object Object]\" and could match a `when.in` entry by\n // accident. No text ⇒ no match, same as an absent field.\n const text = fieldTextOrNull(record[when.field]);\n if (text === null) return false;\n return when.in.includes(text);\n}\n\n/** Minimal shape this helper needs from an action — the optional state\n * gate, whichever name its kind uses: `when` on the seeded kinds\n * (chat/agent), `require` on mutate. Accepts the full CollectionAction\n * union (each variant declares at most one of the two). */\nexport interface ActionWithWhen {\n when?: WhenPredicate;\n require?: WhenPredicate;\n}\n\n/** True when the action's button should render against `record` — and,\n * server-side, whether it may RUN (visibility is the authorization\n * rule, for every kind). */\nexport function actionVisible(action: ActionWithWhen, record: Record<string, unknown>): boolean {\n return whenMatches(action.when ?? action.require, record);\n}\n\n/** The run key naming one in-flight `kind: \"agent\"` action button —\n * written by the server's dispatch guard, read back by the client from\n * the detail response's `runningActions` to drive the spinner. ONE\n * builder (isomorphic) so the two sides can't drift. Collection-level\n * and per-record actions live in distinct namespaces so an id collision\n * between `actions` and `collectionActions` can't alias. */\nexport function agentActionRunKey(actionId: string, itemId?: string): string {\n return itemId === undefined ? `collection/${actionId}` : `item/${itemId}/${actionId}`;\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 resolution for `backlinks` fields (plan step ② of\n// plans/done/collection-ontology.md): the display-only reverse side of `ref`.\n// Both the server enrichment (`server/derive.ts`) and the client detail\n// view derive the row set through THESE helpers, so the LLM (getItems)\n// and the user (record panel) always see the same rows — the same\n// single-implementation rule `deriveAll` follows for formulas. No zod,\n// no I/O; safe for the browser barrel.\n\nimport { whenMatches } from \"./actionVisible\";\nimport { fieldTextOrNull } from \"./fieldText\";\nimport type { CollectionFieldSpec, CollectionItem } from \"./schema\";\n\n/** The `backlinks` member of the field-spec union. */\nexport type BacklinksFieldSpec = Extract<CollectionFieldSpec, { type: \"backlinks\" }>;\n\n/** Does `item` reference `recordId` through `via`? Three shapes, tried in\n * order so the dotted form never regresses an existing flat key:\n *\n * - Exact key (`via` is an own property of `item`): the field holds the id\n * directly — compared as text, like every ref deref. This is checked FIRST\n * because `schemaZ` accepts any field name (`z.string()`), so a field\n * literally named `\"client.id\"` keeps its pre-nesting flat-match behaviour\n * instead of being reinterpreted as a table path. A field holding an\n * array/object has no id to compare, so it matches nothing (rather than\n * testing \"[object Object]\" against the record id).\n * - Nested ref (`via: \"<tableField>.<refColumn>\"`, split on the FIRST `.`,\n * only when no exact key exists): the source stores its ref one level down,\n * inside a `table` field's rows (the ontology scanner advertises these as\n * `${key}.${subKey}`). Matches when `item[tableField]` is an array and ANY\n * row's `refColumn` derefs to `recordId`. A record referencing the target in\n * several rows still matches once — the caller filters over source records,\n * each yielded at most once.\n * - No dot and no such key: matches nothing.\n *\n * Fail-soft throughout: a dotted `via` whose table field is absent / non-array,\n * or whose rows lack the column, matches nothing — the same \"a `via` that\n * doesn't resolve matches nothing\" contract as the flat case. Deeper nesting\n * (`a.b.c`) makes `b.c` the column name, which no row carries → no match. */\nexport function viaMatches(via: string, item: CollectionItem, recordId: string): boolean {\n if (Object.hasOwn(item, via)) return fieldTextOrNull(item[via]) === recordId;\n const dot = via.indexOf(\".\");\n if (dot === -1) return false;\n const tableField = via.slice(0, dot);\n const refColumn = via.slice(dot + 1);\n const rows = item[tableField];\n if (!Array.isArray(rows)) return false;\n return rows.some((row) => fieldTextOrNull((row as CollectionItem)?.[refColumn]) === recordId);\n}\n\n/** The SOURCE records whose `via` field stores `recordId` (compared as\n * strings, like every ref deref), with the optional `filter` applied —\n * in the source items' given order. `via` may be a top-level column or a\n * `<tableField>.<refColumn>` path into a `table` field's rows (see\n * {@link viaMatches}). Fail-soft by construction: a `via` that doesn't\n * resolve on the source records simply matches nothing.\n * Callers pass DERIVED source records, so a `filter`/`display` on a\n * derived column works when its formula is SELF-CONTAINED (an invoice\n * `total` = sum over its own line items); a source column that derefs\n * yet another collection stays absent — the same each-record-derives-\n * against-itself rule ref targets follow. */\nexport function backlinkRows(spec: Pick<BacklinksFieldSpec, \"via\" | \"filter\">, recordId: string, sourceItems: CollectionItem[]): CollectionItem[] {\n if (!recordId) return [];\n return sourceItems.filter((item) => viaMatches(spec.via, item, recordId) && whenMatches(spec.filter, item));\n}\n\n/** Project one backlink row to the keys consumers surface: the source\n * collection's primaryKey (rows must stay addressable — it's the link\n * target) plus the declared `display` columns. Keys the row doesn't\n * carry are simply absent, mirroring `projectFields` in getItems. */\nexport function projectBacklinkRow(row: CollectionItem, display: readonly string[], primaryKey: string): CollectionItem {\n const keys = display.includes(primaryKey) ? display : [primaryKey, ...display];\n // Own-property only (matches `viaMatches` above): a display column named\n // `toString` must be absent, not project an inherited prototype function.\n return Object.fromEntries(keys.filter((key) => Object.hasOwn(row, key)).map((key) => [key, row[key]]));\n}\n\n/** The `rollup` member of the field-spec union. */\nexport type RollupFieldSpec = Extract<CollectionFieldSpec, { type: \"rollup\" }>;\n\n/** Numeric coercion shared by the strict record lint (`./recordZ`) and\n * rollup sums: a plain number, or a non-blank numeric string (renderers\n * coerce those via `Number(...)`, so they display fine). Anything else —\n * arrays (`[]` stringifies to `\"\"` = 0, `[42]` to `\"42\"`), booleans,\n * objects — is NaN. Lives here (zod-free) so both consumers share one\n * definition of \"numeric\". */\nexport function coerceNumeric(value: unknown): number {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\" && value.trim() !== \"\") return Number(value);\n return NaN;\n}\n\n/** The rollup aggregate over the matching source rows (plan step ⑤):\n * `count` = how many rows match; `sum` = the total of `column` over\n * them, skipping non-numeric / absent values (a partially-filled source\n * still sums what's there). An EMPTY match set is a real 0 — the\n * fail-soft null lives at the caller, for a source collection that\n * couldn't be resolved at all. Same derived-source-records contract as\n * `backlinkRows`: pass records derived against themselves, so summing a\n * self-contained derived column (an invoice `total`) works. */\nexport function rollupValue(spec: Pick<RollupFieldSpec, \"via\" | \"filter\" | \"op\" | \"column\">, recordId: string, sourceItems: CollectionItem[]): number {\n const rows = backlinkRows(spec, recordId, sourceItems);\n if (spec.op === \"count\") return rows.length;\n let total = 0;\n for (const row of rows) {\n const value = coerceNumeric(spec.column === undefined ? undefined : row[spec.column]);\n if (Number.isFinite(value)) total += value;\n }\n return total;\n}\n","// The single implementation of \"which linked collections must be\n// preloaded\" for a schema — walked identically by the server (derive /\n// enrich, `server/derive.ts`) and the client (linked-cache fetch,\n// collection-plugin). Both sides MUST agree: the server derefs exactly the\n// collections the client caches, so a new ref-shaped field type is taught\n// here once instead of drifting between two mirrored copies. Pure schema\n// walk, no zod / I/O — safe for the browser barrel.\n\nimport type { CollectionFieldSpec, CollectionSchema } from \"./schema\";\n\n/** Slugs of every collection referenced by a `ref` field — top-level and\n * one level into `table` sub-fields (nested tables are schema-rejected,\n * so a single recursion suffices). */\nexport function uniqueRefTargets(schema: CollectionSchema): string[] {\n const targets = new Set<string>();\n const walk = (fields: Record<string, CollectionFieldSpec>): void => {\n for (const field of Object.values(fields)) {\n if (field.type === \"ref\" && typeof field.to === \"string\" && field.to.length > 0) targets.add(field.to);\n if (field.type === \"table\" && field.of) walk(field.of);\n }\n };\n walk(schema.fields);\n return [...targets];\n}\n\n/** Slugs of every collection referenced by an `embed` field. Top-level\n * only — the schema rejects `embed` inside a table's `of`, so no\n * recursion. */\nexport function uniqueEmbedTargets(schema: CollectionSchema): string[] {\n const targets = new Set<string>();\n for (const field of Object.values(schema.fields)) {\n if (field.type === \"embed\" && typeof field.to === \"string\" && field.to.length > 0) targets.add(field.to);\n }\n return [...targets];\n}\n\n/** Slugs of every SOURCE collection a `backlinks` or `rollup` field\n * reverses over — loaded once (the two field kinds share one load).\n * Top-level only, like `embed` (the schema rejects both inside a table's\n * `of`). */\nexport function uniqueBacklinkSources(schema: CollectionSchema): string[] {\n const sources = new Set<string>();\n for (const field of Object.values(schema.fields)) {\n if ((field.type === \"backlinks\" || field.type === \"rollup\") && field.from.length > 0) sources.add(field.from);\n }\n return [...sources];\n}\n","// Pure SQL-like `where` predicate for `dynamicIcon` (see\n// `DynamicIconSource.where` / `DynamicIconRule.where` in `./schema`).\n// An AND of typed conditions — richer than the single-field `CollectionWhen`\n// used elsewhere (fields/actions via `./actionVisible`), which stays as-is\n// for its existing callers. No fs, no host state. The condition SHAPES are\n// derived from the zod source of truth in `./schemaZ` (type-only imports —\n// this evaluator stays zod-free at runtime).\n\nimport type { z } from \"zod\";\nimport type { ValueRefZ, WhereCondZ, WhereZ } from \"./schemaZ\";\n\n/** Reads the comparison value from a field instead of a schema literal:\n * - `record` set → another record: `recordsById[record][field]` (e.g. a\n * `_config` singleton's `defaultCity`, following a per-user setting);\n * - `record` omitted → the SAME record being matched (field-to-field, e.g.\n * `spent > budget`). */\nexport type ValueRef = z.infer<typeof ValueRefZ>;\n\n/** One typed condition: `record[field] <op> value`. The comparison value is\n * either a literal `value` (a plain string for every op except `in`, which\n * takes the allowed set) or a `valueFrom` reference resolved against the\n * `recordsById` map passed to `matchesWhere`. Exactly one of the two is\n * expected — enforced by zod at the schema boundary (`./schemaZ`), not\n * here. */\nexport type WhereCond = z.infer<typeof WhereCondZ>;\n\n/** Comparison operators one `WhereCond` may apply to `record[field]`. */\nexport type WhereOp = WhereCond[\"op\"];\n\n/** A `where` clause is the AND of its conditions — every one must match. */\nexport type Where = z.infer<typeof WhereZ>;\n\n/** True when `record[field]` is absent (`undefined`/`null`) — the only case\n * where `ne` and every other op disagree on the result. */\nfunction isMissing(raw: unknown): boolean {\n return raw === undefined || raw === null;\n}\n\n/** Own-property read for a user/LLM-controlled key (`cond.field`,\n * `valueFrom.record`, `valueFrom.field`). A bare `obj[key]` reaches inherited\n * Object.prototype members, so `field: \"toString\"` would read a function\n * instead of an absent value (spurious match), and `record: \"constructor\"`\n * would read the `Object` function whose `.name`/`.length` are plausible\n * bogus comparands — breaking the \"unresolved ⇒ never matches\" contract\n * (#2323). A prototype key resolves to `undefined` (absent). */\nfunction ownProp<T>(obj: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(obj, key) ? obj[key] : undefined;\n}\n\n/** The effective comparison value for `cond`: its literal `value`, or — for\n * a `valueFrom` reference — the target field read out of `recordsById`.\n * `undefined` means UNRESOLVED (no such record, or the field on it is\n * missing); the caller must treat that as \"never matches\", not as a\n * literal `undefined` value to compare against. */\nfunction resolveValue(cond: WhereCond, record: Record<string, unknown>, recordsById: Record<string, Record<string, unknown>>): string | string[] | undefined {\n if (!cond.valueFrom) return cond.value;\n const { record: refRecord, field } = cond.valueFrom;\n const target = refRecord === undefined ? record : ownProp(recordsById, refRecord);\n const raw = target === undefined ? undefined : ownProp(target, field);\n return isMissing(raw) ? undefined : String(raw);\n}\n\nfunction matchesNumericOp(operator: \"gt\" | \"gte\" | \"lt\" | \"lte\", left: number, right: number): boolean {\n if (operator === \"gt\") return left > right;\n if (operator === \"gte\") return left >= right;\n if (operator === \"lt\") return left < right;\n return left <= right;\n}\n\n/** `Number(\"\")` / `Number(\" \")` are `0`, not `NaN`, so treat a blank string\n * as non-numeric explicitly — an empty field must fail a numeric compare,\n * not read as zero. */\nfunction toNumber(raw: string): number {\n return raw.trim() === \"\" ? NaN : Number(raw);\n}\n\nfunction matchesNumeric(operator: \"gt\" | \"gte\" | \"lt\" | \"lte\", raw: string, value: string | string[]): boolean {\n if (Array.isArray(value)) return false;\n const left = toNumber(raw);\n const right = toNumber(value);\n if (Number.isNaN(left) || Number.isNaN(right)) return false;\n return matchesNumericOp(operator, left, right);\n}\n\n/** True when the present string `raw` satisfies `operator` against the\n * resolved `value` (field known to exist — MISSING is handled by the\n * caller before this runs, and an UNRESOLVED `valueFrom` never reaches\n * here either). */\nfunction matchesPresent(operator: WhereOp, raw: string, value: string | string[]): boolean {\n switch (operator) {\n case \"eq\":\n return raw === String(value);\n case \"ne\":\n return raw !== String(value);\n case \"in\":\n return Array.isArray(value) && value.includes(raw);\n case \"contains\":\n return raw.includes(String(value));\n case \"gt\":\n case \"gte\":\n case \"lt\":\n case \"lte\":\n return matchesNumeric(operator, raw, value);\n default:\n return false;\n }\n}\n\n/** True when `record` satisfies one condition, given `recordsById` to\n * resolve a `valueFrom` reference. Two independent MISSING cases, checked\n * in order:\n * - `record[cond.field]` absent (`undefined`/`null`) → matches only `ne`\n * (vacuously true — \"not equal to X\" holds when there's no value at\n * all); every other op is false. Unchanged from the literal-`value`\n * behaviour, regardless of whether `valueFrom` would also resolve.\n * - the resolved comparison value is UNRESOLVED (a `valueFrom` whose\n * target record/field doesn't exist) → false for EVERY op, including\n * `ne` — a broken reference must never spuriously match. */\nfunction matchesCond(cond: WhereCond, record: Record<string, unknown>, recordsById: Record<string, Record<string, unknown>>): boolean {\n const raw = ownProp(record, cond.field);\n if (isMissing(raw)) return cond.op === \"ne\";\n const value = resolveValue(cond, record, recordsById);\n if (value === undefined) return false;\n return matchesPresent(cond.op, String(raw), value);\n}\n\n/** True when `record` satisfies every condition in `where` (AND). An empty\n * `where` matches everything. `recordsById` — the source collection's\n * records keyed by primaryKey — resolves any `valueFrom` reference;\n * omitted (default `{}`) for callers with no cross-record lookups, in\n * which case every `valueFrom` condition is UNRESOLVED and so never\n * matches. */\nexport function matchesWhere(where: Where, record: Record<string, unknown>, recordsById: Record<string, Record<string, unknown>> = {}): boolean {\n return where.every((cond) => matchesCond(cond, record, recordsById));\n}\n","// The \"is this record done?\" predicate — THE single implementation,\n// shared by the notification reconciler (bell clearing, collection-watchers),\n// spawn (successor-predicate fallback, ../server/spawn.ts), and view-side\n// completion filters. Zod-free and I/O-free like the rest of `core/`, so\n// it is browser-safe through the collection barrel.\n//\n// Two completion forms (see `CollectionSchemaZ`'s completion refine):\n// - legacy pair: `completionField` names a stored field and\n// `completionDoneValues` lists the values that mean done —\n// done ⇔ `String(item[completionField])` ∈ `completionDoneValues`.\n// - flag form: `completionField` names a `flag` field (and\n// `completionDoneValues` is absent) — done ⇔ the flag's `where`\n// matches. Evaluated directly against the raw record here (NOT read\n// from a materialized value) because callers like the reconciler and\n// spawn work on records straight off disk, before any `deriveAll`\n// enrichment. That raw evaluation is CORRECT BY CONSTRUCTION: a\n// schema-level refine rejects a completion flag whose `where`\n// references computed fields, so every condition reads stored data.\n\nimport { fieldText, fieldTextOrNull } from \"./fieldText\";\nimport { matchesWhere, type Where } from \"./where\";\nimport type { CollectionFieldSpec, CollectionItem } from \"./schema\";\n\n/** The slice of a parsed schema the done predicate reads — minimal\n * structural shape (like `DerivableSchema`) so the client and server\n * `CollectionSchema` types both satisfy it as-is. */\nexport interface CompletionSchemaView {\n /** Optional so legacy-pair callers (and their test fixtures) that\n * never consult field specs keep working; only the flag form needs\n * to look the completion field up. */\n fields?: Record<string, { type: string; where?: Where }>;\n completionField?: string;\n completionDoneValues?: readonly string[];\n}\n\n/** True iff the schema declares completion tracking AND `item` is done\n * under whichever completion form the schema uses (see module doc). */\nexport function itemIsDone(schema: CompletionSchemaView, item: Record<string, unknown>): boolean {\n const { completionField, completionDoneValues } = schema;\n if (!completionField) return false;\n const spec = schema.fields?.[completionField];\n if (spec?.type === \"flag\" && spec.where) return matchesWhere(spec.where, item);\n if (!completionDoneValues) return false;\n // An array/object field has no text form; treat it as \"not done\" rather than\n // letting \"[object Object]\" match a configured done-value.\n const text = fieldTextOrNull(item[completionField]);\n if (text === null) return false;\n return completionDoneValues.includes(text);\n}\n\n/** The schema slice `completionCoveredByFieldChip` reads: field kinds (it\n * inspects `boolean` / `toggle` variants' `field` / `onValue`) plus the\n * completion pair. Minimal structural shape so both the client and server\n * `CollectionSchema` types satisfy it as-is. */\nexport interface CompletionChipSchemaView {\n fields: Record<string, { type: string; field?: string; onValue?: string }>;\n completionField?: string;\n completionDoneValues?: readonly string[];\n}\n\n/** True when an existing FIELD chip already expresses the legacy completion\n * predicate exactly, so a synthesized \"done\" chip would be a duplicate: a\n * boolean `completionField` (done ⇔ `\"true\"` ⇔ the boolean's own chip), or a\n * `toggle` projecting the `completionField` whose `onValue` is the single\n * done value (the todos-schema shape: toggle \"Done\" on `status` +\n * `completionDoneValues: [\"done\"]`). A superset pair (extra done values)\n * still synthesizes — no field chip covers it. */\nexport function completionCoveredByFieldChip(schema: CompletionChipSchemaView): boolean {\n const { completionField, completionDoneValues } = schema;\n if (completionDoneValues?.length !== 1) return false;\n const [doneValue] = completionDoneValues;\n if (schema.fields[completionField ?? \"\"]?.type === \"boolean\") return doneValue === \"true\";\n return Object.values(schema.fields).some((field) => field.type === \"toggle\" && field.field === completionField && field.onValue === doneValue);\n}\n\n/** Whether a `toggle` field reads as checked: its projected enum field currently\n * equals `onValue`. The toggle stores nothing of its own, so this reads the raw\n * projected value; a non-toggle field is never checked. */\nexport function toggleChecked(item: CollectionItem, field: CollectionFieldSpec): boolean {\n return field.type === \"toggle\" && fieldText(item[field.field]) === field.onValue;\n}\n\n/** A `flag` FIELD's boolean for one row, read off the already-enriched record\n * (so a flag computed from derived/rollup inputs is correct). Strict `=== true`\n * on purpose: a truthy non-boolean (\"yes\", 1) is NOT a set flag, so a stray\n * value can never render as an active flag. */\nexport function flagFieldValue(record: Record<string, unknown>, key: string): boolean {\n return record[key] === true;\n}\n\n/** One entry in the table's flag-filter menu: a real `flag` / `boolean` /\n * `toggle` field, or the synthesized legacy-completion chip (`synthetic`,\n * predicate = `itemIsDone`). */\nexport interface FlagChip {\n key: string;\n label: string;\n synthetic?: boolean;\n}\n\n/** The schema slice `chipMatches` reads: full field specs (it inspects the\n * `toggle` variant's `field` / `onValue`) plus the completion pair that\n * `itemIsDone` needs for the synthesized chip. */\nexport interface ChipMatchSchema {\n fields: Record<string, CollectionFieldSpec>;\n completionField?: string;\n completionDoneValues?: readonly string[];\n}\n\n/** Whether one row satisfies a chip's predicate: `itemIsDone` for the\n * synthesized completion chip, the projected value for a `toggle`, the stored\n * boolean for a `boolean`, else the computed flag value. The flag branch reads\n * the ENRICHED record via the injected `deriveRecord` (kept a parameter so this\n * stays pure and framework-free). */\nexport function chipMatches(\n chip: FlagChip,\n schema: ChipMatchSchema,\n item: CollectionItem,\n deriveRecord: (item: CollectionItem) => Record<string, unknown>,\n): boolean {\n if (chip.synthetic) return itemIsDone(schema, item);\n const field = schema.fields[chip.key];\n if (field?.type === \"toggle\") return toggleChecked(item, field);\n if (field?.type === \"boolean\") return item[chip.key] === true;\n return flagFieldValue(deriveRecord(item), chip.key);\n}\n","// Pure resolver for a collection's dynamic launcher-shortcut icon (see\n// `CollectionSchema.dynamicIcon`). Selects one \"source\" record from a\n// (possibly cross-collection, optionally `where`-filtered) records pool,\n// then maps it through a first-match-wins rules list to an icon name.\n// No fs, no host state — the server-side compute\n// (`packages/core/src/collection/server/dynamicIcon.ts`) loads the source\n// collection's records and calls these.\n\nimport { fieldText } from \"./fieldText\";\nimport { matchesWhere } from \"./where\";\nimport type { CollectionFieldSpec, CollectionItem, CollectionSchema, DynamicIconSource, DynamicIconSpec } from \"./schema\";\n\n/** The record with the greatest `String(record[field])` (localeCompare) —\n * ties keep the first-seen record (stable left-to-right `reduce`). */\nfunction latestByField(pool: CollectionItem[], field: string): CollectionItem {\n return pool.reduce((latest, candidate) => (fieldText(candidate[field]).localeCompare(fieldText(latest[field])) > 0 ? candidate : latest));\n}\n\n/** Reduce `records` to the one record that decides the effective icon, per\n * `source`'s `where` filter + `from` strategy:\n * - pool = `source.where`-filtered records, or every record when unset;\n * - an empty pool resolves to `null` (no source record → fallback);\n * - `from: \"first\"` / `\"when\"` → the first pool record (storage order);\n * - `from: \"latest\"` (default), with `orderBy` given → the pool record\n * whose `String(record[orderBy])` sorts highest;\n * - `from: \"latest\"`, with no `orderBy` → the last pool record.\n * `recordsById` (the source collection's records keyed by primaryKey)\n * resolves any `valueFrom` reference inside `source.where`; omitted for\n * callers with no cross-record lookups. */\nexport function selectDynamicRecord(\n records: CollectionItem[],\n source: DynamicIconSource,\n orderBy: string | undefined,\n recordsById: Record<string, CollectionItem> = {},\n): CollectionItem | null {\n const { where } = source;\n const pool = where ? records.filter((record) => matchesWhere(where, record, recordsById)) : records;\n if (pool.length === 0) return null;\n if (source.from === \"first\" || source.from === \"when\") return pool[0];\n return orderBy ? latestByField(pool, orderBy) : pool[pool.length - 1];\n}\n\n/** Map a resolved source record to the effective icon: `spec.fallback`\n * (or the collection's own static `icon`) when there's no record or no\n * rule matches; otherwise the `icon` of the first rule whose `where`\n * matches the record. `recordsById` resolves any `valueFrom` reference\n * inside a rule's `where`, same as `selectDynamicRecord`. */\nexport function resolveIcon(\n record: CollectionItem | null,\n spec: DynamicIconSpec,\n staticIcon: string,\n recordsById: Record<string, CollectionItem> = {},\n): string {\n const fallback = spec.fallback ?? staticIcon;\n if (!record) return fallback;\n const matched = spec.rules.find((rule) => matchesWhere(rule.where, record, recordsById));\n return matched ? matched.icon : fallback;\n}\n\nconst isDateLikeField = (field: CollectionFieldSpec): boolean => field.type === \"date\" || field.type === \"datetime\";\n\n/** The first field key (declaration order) whose type is `date` or\n * `datetime` — the default `orderBy` for `from: \"latest\"` when a\n * `DynamicIconSource` doesn't name one. `undefined` when the schema has\n * no date-like field. */\nexport function firstDateField(schema: CollectionSchema): string | undefined {\n return Object.entries(schema.fields).find(([, field]) => isDateLikeField(field))?.[0];\n}\n","// Tiny expression evaluator for the `derived` field type on\n// schema-driven collections (see plans/done/feat-mc-invoice.md).\n//\n// Grammar (recursive-descent, no precedence climbing — six\n// non-terminals total):\n//\n// expr := term (('+' | '-') term)*\n// term := factor (('*' | '/') factor)*\n// factor := number | sumCall | refAccess | identifier | '(' expr ')'\n// sumCall:= 'sum' '(' sumArg ')'\n// sumArg := tableCol (('*' | '/') tableCol)* // e.g. lineItems[].quantity * lineItems[].rate\n// tableCol := identifier '[]' '.' identifier\n// refAccess := identifier '.' identifier // e.g. ticker.price — deref a ref field into its target record\n//\n// `identifier` accepts top-level field names (single segment).\n// Inside `sumArg`, identifiers are the `<table>[].col` form.\n// A two-segment `<field>.<col>` at factor level is a *ref deref*:\n// `<field>` must be a `ref`-typed field on this record (its stored\n// value is the target item's slug), and `<col>` is a numeric column\n// read from that target record. The caller resolves the target into\n// `ctx.refs` (it owns the schema + the loaded target collection);\n// the evaluator stays pure and never does I/O.\n//\n// What's deliberately NOT supported (and would parse-error rather\n// than silently misbehave):\n// - String literals, boolean operators, comparisons, conditionals\n// - Nested function calls beyond `sum(...)`\n// - Anything in the record that isn't a number / table-of-objects\n//\n// All evaluation is pure — no eval(), no Function constructor.\n// Returns `null` on any failure (parse error, unbound identifier,\n// non-finite arithmetic). The caller renders `null` as em-dash in\n// the table cell + form display.\n\nexport interface FormulaContext {\n /** The record being evaluated. For derived fields in the form,\n * this is the live draft (text + table both converted via the\n * same `draftToRecord` pipeline). For the main table cell,\n * this is the persisted item. */\n record: Record<string, unknown>;\n /** Resolved ref-target records for THIS row, keyed by the local\n * `ref` field name. The caller (which has the schema + the linked\n * collection's items loaded) maps each ref field's stored slug to\n * the full target record and passes it here, so a `<field>.<col>`\n * formula can read a numeric column off the referenced record\n * (e.g. `shares * ticker.price`). A missing key or `null` value\n * (unknown field / dangling slug) makes that deref evaluate to\n * NaN → the whole formula returns `null` → em-dash, consistent\n * with every other failure mode. Absent ⇒ no refs available. */\n refs?: Record<string, Record<string, unknown> | null>;\n}\n\nexport function evaluateDerived(formula: string, ctx: FormulaContext): number | null {\n let tokens: Token[];\n try {\n tokens = tokenize(formula);\n } catch {\n return null;\n }\n // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Parser class is defined later in the file (grouped with its AST + evaluator); evaluateDerived runs after module init so the TDZ concern doesn't apply.\n const parser = new Parser(tokens);\n let ast: Node;\n try {\n ast = parser.parseExpr();\n if (!parser.atEnd()) return null; // trailing junk\n } catch {\n return null;\n }\n const value = evaluate(ast, ctx);\n return Number.isFinite(value) ? value : null;\n}\n\n// ─── Tokens ────────────────────────────────────────────────\n\ntype TokenKind = \"number\" | \"ident\" | \"(\" | \")\" | \"+\" | \"-\" | \"*\" | \"/\" | \"[]\" | \".\";\n\ninterface Token {\n kind: TokenKind;\n value?: string | number;\n}\n\nconst SINGLE_CHAR_PUNCT = new Set<TokenKind>([\"(\", \")\", \"+\", \"-\", \"*\", \"/\", \".\"]);\n\ninterface Cursor {\n input: string;\n index: number;\n}\n\nfunction consumeWhitespace(cur: Cursor): boolean {\n const char = cur.input[cur.index];\n if (char === \" \" || char === \"\\t\" || char === \"\\n\") {\n cur.index++;\n return true;\n }\n return false;\n}\n\nfunction consumeNumber(cur: Cursor): Token | null {\n const char = cur.input[cur.index] ?? \"\";\n const next = cur.input[cur.index + 1] ?? \"\";\n if (!isDigit(char) && !(char === \".\" && isDigit(next))) return null;\n let raw = \"\";\n while (cur.index < cur.input.length) {\n const here = cur.input[cur.index] ?? \"\";\n if (!isDigit(here) && here !== \".\") break;\n raw += here;\n cur.index++;\n }\n const num = Number(raw);\n if (!Number.isFinite(num)) throw new Error(\"bad number\");\n return { kind: \"number\", value: num };\n}\n\nfunction consumeIdent(cur: Cursor): Token | null {\n const char = cur.input[cur.index] ?? \"\";\n if (!isIdentStart(char)) return null;\n let raw = \"\";\n while (cur.index < cur.input.length && isIdentChar(cur.input[cur.index] ?? \"\")) {\n raw += cur.input[cur.index];\n cur.index++;\n }\n return { kind: \"ident\", value: raw };\n}\n\nfunction consumePunct(cur: Cursor): Token | null {\n const char = cur.input[cur.index] ?? \"\";\n if (char === \"[\" && cur.input[cur.index + 1] === \"]\") {\n cur.index += 2;\n return { kind: \"[]\" };\n }\n if (SINGLE_CHAR_PUNCT.has(char as TokenKind)) {\n cur.index++;\n return { kind: char as TokenKind };\n }\n return null;\n}\n\nfunction tokenize(input: string): Token[] {\n const tokens: Token[] = [];\n const cur: Cursor = { input, index: 0 };\n while (cur.index < input.length) {\n if (consumeWhitespace(cur)) continue;\n // Number FIRST so a leading-dot literal (`.25`) isn't split by\n // the `.` punctuation branch.\n const numTok = consumeNumber(cur);\n if (numTok) {\n tokens.push(numTok);\n continue;\n }\n const punctTok = consumePunct(cur);\n if (punctTok) {\n tokens.push(punctTok);\n continue;\n }\n const identTok = consumeIdent(cur);\n if (identTok) {\n tokens.push(identTok);\n continue;\n }\n throw new Error(`unexpected char ${input[cur.index]}`);\n }\n return tokens;\n}\n\nfunction isDigit(char: string): boolean {\n return char >= \"0\" && char <= \"9\";\n}\nfunction isIdentStart(char: string): boolean {\n return (char >= \"a\" && char <= \"z\") || (char >= \"A\" && char <= \"Z\") || char === \"_\";\n}\nfunction isIdentChar(char: string): boolean {\n return isIdentStart(char) || isDigit(char);\n}\n\n// ─── AST + Parser ───────────────────────────────────────────\n\ntype Node =\n | { kind: \"num\"; value: number }\n | { kind: \"ident\"; name: string }\n | { kind: \"ref\"; field: string; col: string }\n | { kind: \"binop\"; operator: \"+\" | \"-\" | \"*\" | \"/\"; left: Node; right: Node }\n | { kind: \"sum\"; arg: SumArg };\n\ninterface SumArg {\n // factors multiplied/divided together; each is a (tableName, colName) ref into a row.\n factors: { table: string; col: string }[];\n /** Operators between factors: length = factors.length - 1; each\n * is \"*\" or \"/\". For a single-factor sum (`sum(lineItems[].amount)`)\n * this is empty. */\n operators: (\"*\" | \"/\")[];\n}\n\nclass Parser {\n private cursor = 0;\n constructor(private readonly tokens: Token[]) {}\n\n atEnd(): boolean {\n return this.cursor >= this.tokens.length;\n }\n private peek(): Token | undefined {\n return this.tokens[this.cursor];\n }\n private consume(): Token {\n const tok = this.tokens[this.cursor++];\n if (!tok) throw new Error(\"unexpected end of input\");\n return tok;\n }\n private expect(kind: TokenKind): Token {\n const tok = this.consume();\n if (tok.kind !== kind) throw new Error(`expected ${kind}, got ${tok.kind}`);\n return tok;\n }\n\n parseExpr(): Node {\n let left = this.parseTerm();\n while (this.peek()?.kind === \"+\" || this.peek()?.kind === \"-\") {\n const operator = this.consume().kind as \"+\" | \"-\";\n const right = this.parseTerm();\n left = { kind: \"binop\", operator, left, right };\n }\n return left;\n }\n\n private parseTerm(): Node {\n let left = this.parseFactor();\n while (this.peek()?.kind === \"*\" || this.peek()?.kind === \"/\") {\n const operator = this.consume().kind as \"*\" | \"/\";\n const right = this.parseFactor();\n left = { kind: \"binop\", operator, left, right };\n }\n return left;\n }\n\n private parseFactor(): Node {\n const tok = this.peek();\n if (!tok) throw new Error(\"unexpected end in factor\");\n if (tok.kind === \"number\") {\n this.consume();\n return { kind: \"num\", value: tok.value as number };\n }\n if (tok.kind === \"(\") {\n this.consume();\n const inner = this.parseExpr();\n this.expect(\")\");\n return inner;\n }\n if (tok.kind === \"ident\") {\n const name = (tok.value as string) ?? \"\";\n // sum(...) — only function call we support\n if (name === \"sum\" && this.tokens[this.cursor + 1]?.kind === \"(\") {\n this.consume(); // ident\n this.expect(\"(\");\n const arg = this.parseSumArg();\n this.expect(\")\");\n return { kind: \"sum\", arg };\n }\n this.consume(); // ident\n // ref deref: `<field>.<col>` (e.g. ticker.price). The table-row\n // form `<table>[].col` only appears inside sum(), so a `.`\n // immediately after a top-level ident is unambiguously a ref\n // dereference here.\n if (this.peek()?.kind === \".\") {\n this.consume(); // '.'\n const col = this.expect(\"ident\");\n return { kind: \"ref\", field: name, col: col.value as string };\n }\n return { kind: \"ident\", name };\n }\n throw new Error(`unexpected token ${tok.kind} in factor`);\n }\n\n private parseSumArg(): SumArg {\n const factors: { table: string; col: string }[] = [];\n const operators: (\"*\" | \"/\")[] = [];\n factors.push(this.parseTableCol());\n while (this.peek()?.kind === \"*\" || this.peek()?.kind === \"/\") {\n const operator = this.consume().kind as \"*\" | \"/\";\n operators.push(operator);\n factors.push(this.parseTableCol());\n }\n return { factors, operators };\n }\n\n private parseTableCol(): { table: string; col: string } {\n const tableTok = this.expect(\"ident\");\n this.expect(\"[]\");\n this.expect(\".\");\n const colTok = this.expect(\"ident\");\n return { table: tableTok.value as string, col: colTok.value as string };\n }\n}\n\n// ─── Evaluator ──────────────────────────────────────────────\n\nfunction evaluate(node: Node, ctx: FormulaContext): number {\n if (node.kind === \"num\") return node.value;\n if (node.kind === \"ident\") {\n const raw = ctx.record[node.name];\n return toFiniteNumber(raw);\n }\n if (node.kind === \"ref\") {\n // `<field>.<col>`: read `col` off the resolved target record the\n // caller put in ctx.refs. Unknown field / dangling slug → null →\n // NaN, so the whole formula fails soft to an em-dash.\n const target = ctx.refs?.[node.field] ?? null;\n if (!target) return Number.NaN;\n return toFiniteNumber(target[node.col]);\n }\n if (node.kind === \"binop\") {\n const left = evaluate(node.left, ctx);\n const right = evaluate(node.right, ctx);\n return applyBinop(node.operator, left, right);\n }\n if (node.kind === \"sum\") {\n return evaluateSum(node.arg, ctx);\n }\n // Exhaustive — TS narrows above branches but throw keeps runtime honest.\n throw new Error(`unknown node`);\n}\n\nfunction applyBinop(operator: \"+\" | \"-\" | \"*\" | \"/\", left: number, right: number): number {\n if (!Number.isFinite(left) || !Number.isFinite(right)) return Number.NaN;\n if (operator === \"+\") return left + right;\n if (operator === \"-\") return left - right;\n if (operator === \"*\") return left * right;\n // operator === \"/\"\n if (right === 0) return Number.NaN;\n return left / right;\n}\n\nfunction evaluateSum(arg: SumArg, ctx: FormulaContext): number {\n if (arg.factors.length === 0) return 0;\n const tableName = arg.factors[0].table;\n // All factors must reference the SAME table (you can't multiply\n // a row from lineItems against a row from another table — the\n // semantics would be ambiguous). Reject mismatch.\n for (const factor of arg.factors) {\n if (factor.table !== tableName) return Number.NaN;\n }\n const rows = ctx.record[tableName];\n if (!Array.isArray(rows)) return 0;\n let total = 0;\n for (const row of rows) {\n if (!row || typeof row !== \"object\") continue;\n let product = toFiniteNumber((row as Record<string, unknown>)[arg.factors[0].col]);\n if (!Number.isFinite(product)) return Number.NaN;\n for (let i = 1; i < arg.factors.length; i++) {\n const value = toFiniteNumber((row as Record<string, unknown>)[arg.factors[i].col]);\n if (!Number.isFinite(value)) return Number.NaN;\n product = applyBinop(arg.operators[i - 1], product, value);\n }\n total += product;\n }\n return total;\n}\n\nfunction toFiniteNumber(value: unknown): number {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : Number.NaN;\n if (typeof value === \"string\" && value.length > 0) {\n const num = Number(value);\n return Number.isFinite(num) ? num : Number.NaN;\n }\n return Number.NaN;\n}\n","// Own-property read for a plain object indexed by a user/LLM/feed-controlled\n// key. Extracted so the ref/embed resolution in `deriveAll` and `derive`\n// share one guard (where.ts keeps its own private copy for the dynamicIcon\n// `where` evaluator — #2443).\n\n/** Read `obj[key]` only when it is an OWN property. A bare `obj[key]` reaches\n * inherited Object.prototype members, so a dangling ref/embed id like\n * `\"constructor\"` or `\"__proto__\"` resolves to a prototype value (the\n * `Object` function) instead of being absent — breaking the \"missing target\n * ⇒ null (em-dash)\" fail-soft contract and putting a non-serializable\n * function into the API response, where `JSON.stringify` drops the key\n * entirely (#2322). A prototype key resolves to `undefined`; a record whose\n * id is legitimately `\"constructor\"` (an own key) still resolves. */\nexport function ownProp<T>(obj: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(obj, key) ? obj[key] : undefined;\n}\n","// The derived-field saturation loop for schema-driven collections,\n// extracted from `composables/collections/useCollectionRendering.ts` so\n// the server (manageCollection getItems enrichment) and the client\n// (table cells, form display) evaluate formulas through ONE\n// implementation — if the two ever diverged, the UI and the LLM would\n// disagree on a number. Pure module: no Vue, no I/O.\n//\n// Like `actionVisible.ts`, the input types are minimal structural\n// shapes so both the client `FieldSpec`/`CollectionSchema`\n// (src/components/collectionTypes.ts) and the server\n// `CollectionFieldSpec`/`CollectionSchema`\n// (server/workspace/collections/types.ts) satisfy them as-is.\n\nimport { evaluateDerived, type FormulaContext } from \"./derivedFormula\";\nimport { ownProp } from \"./ownProp\";\nimport { matchesWhere, type Where } from \"./where\";\n\n/** Minimal field shape the derive loop needs — accepts both the client\n * FieldSpec and the server CollectionFieldSpec. */\nexport interface DerivableFieldSpec {\n type: string;\n /** When type === \"ref\": slug of the target collection. */\n to?: string;\n /** When type === \"derived\": formula evaluated against the record. */\n formula?: string;\n /** When type === \"flag\": predicate matched against the record. */\n where?: Where;\n}\n\n/** Minimal schema shape: just the ordered field map. */\nexport interface DerivableSchema {\n fields: Record<string, DerivableFieldSpec>;\n}\n\nexport type DerivableRecord = Record<string, unknown>;\n\n/** Per-target-collection cache of loaded referenced records:\n * target collection slug → item slug → full record. Mirrors the\n * client's `RefRecordCache` / the server's enrichment loader. */\nexport type DeriveRefRecords = Record<string, Record<string, DerivableRecord>>;\n\n/** Map each `ref` field's stored slug to its loaded target record (or\n * null when dangling / not loaded), keyed by the LOCAL field name —\n * the shape `evaluateDerived` reads for `<field>.<col>` derefs. */\nexport function resolveRowRefs(schema: DerivableSchema, record: DerivableRecord, refRecords: DeriveRefRecords): NonNullable<FormulaContext[\"refs\"]> {\n const refs: NonNullable<FormulaContext[\"refs\"]> = {};\n for (const [key, field] of Object.entries(schema.fields)) {\n if (field.type !== \"ref\" || !field.to) continue;\n const slug = record[key];\n const targets = ownProp(refRecords, field.to);\n refs[key] = typeof slug === \"string\" && targets ? (ownProp(targets, slug) ?? null) : null;\n }\n return refs;\n}\n\n/** True for the field types the saturation loop below (re)computes:\n * `derived` formulas and `flag` predicates. */\nfunction isLoopComputed(field: DerivableFieldSpec): boolean {\n return field.type === \"derived\" || field.type === \"flag\";\n}\n\n/** The value one loop-computed field takes against the current\n * `enriched` record: a `derived` formula result (`null` = failed) or a\n * `flag` predicate match (total — always a boolean). `undefined` for\n * every other field type (nothing to compute). */\nfunction computeFieldValue(\n field: DerivableFieldSpec,\n enriched: DerivableRecord,\n refs: NonNullable<FormulaContext[\"refs\"]>,\n): number | boolean | null | undefined {\n if (field.type === \"derived\" && field.formula) return evaluateDerived(field.formula, { record: enriched, refs });\n if (field.type === \"flag\" && field.where) return matchesWhere(field.where, enriched);\n return undefined;\n}\n\n/** Evaluate every `derived` and `flag` field against `base`, saturating\n * so a computed field can read another one computed in an earlier pass\n * (`subtotal → tax → total` converges in ≤ field-count passes; a flag\n * may read a derived value, or another flag via its stringified\n * boolean). Cycles can't loop forever — passes are bounded by the\n * number of computed fields and the loop breaks as soon as a pass\n * changes nothing. Failed formulas stay ABSENT (the UI renders them as\n * em-dash); flags are total (always true/false). Returns a copy;\n * `base` is never mutated.\n *\n * Computed keys already present in `base` are stripped before\n * evaluation: computed output is host-truth, never persisted-input\n * fallback. A record JSON can carry a stale (or forged) computed value\n * — raw Write/Edit, legacy data — and without the strip, a failing\n * formula would silently surface that value as if the host computed\n * it. */\nexport function deriveAll(schema: DerivableSchema, base: DerivableRecord, refRecords: DeriveRefRecords): DerivableRecord {\n const computedKeys = new Set(\n Object.entries(schema.fields)\n .filter(([, field]) => isLoopComputed(field))\n .map(([key]) => key),\n );\n const enriched: DerivableRecord = Object.fromEntries(Object.entries(base).filter(([key]) => !computedKeys.has(key)));\n const refs = resolveRowRefs(schema, base, refRecords);\n for (let pass = 0; pass < computedKeys.size; pass++) {\n let mutated = false;\n for (const [key, field] of Object.entries(schema.fields)) {\n const next = computeFieldValue(field, enriched, refs);\n if (next !== undefined && next !== null && enriched[key] !== next) {\n enriched[key] = next;\n mutated = true;\n }\n }\n if (!mutated) break;\n }\n return enriched;\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,36 +0,0 @@
1
- import { collection, doc } from "firebase/firestore";
2
- //#region src/remote-host/health.ts
3
- var RUNNER_HEALTH_STATES = [
4
- "online",
5
- "reconnecting",
6
- "offline"
7
- ];
8
- var isRecord = (value) => typeof value === "object" && value !== null;
9
- var isRunnerHealthState = (value) => RUNNER_HEALTH_STATES.some((state) => state === value);
10
- /** Narrows a parsed HTTP payload. The client renders whatever this accepts, so a
11
- * half-shaped health has to read as "no health reported" rather than as a state. */
12
- var isRunnerHealth = (value) => isRecord(value) && isRunnerHealthState(value.state) && (value.lastError === null || typeof value.lastError === "string") && typeof value.changedAt === "number";
13
- //#endregion
14
- //#region src/remote-host/index.ts
15
- /** Widen a JSON-shaped handler payload to the channel's `JsonObject`.
16
- *
17
- * Exists so the `Jsonify` reasoning above lives in ONE place. Before this,
18
- * eight remote-host handlers each carried their own `as unknown as JsonObject`
19
- * with the justification re-argued in eight slightly different comments —
20
- * which is how a rule stops being reviewable. */
21
- var toJsonObject = (payload) => payload;
22
- var isExpired = (command, now) => typeof command.expiresAt === "number" && now >= command.expiresAt;
23
- var byCreatedAt = (left, right) => (left.createdAt ?? 0) - (right.createdAt ?? 0);
24
- var REMOTE_HOST_PROTOCOL_VERSION = 2;
25
- var buildHostPresence = (channel, handlers, online) => ({
26
- online,
27
- hostId: channel.hostId,
28
- protocolVersion: 2,
29
- capabilities: Object.keys(handlers)
30
- });
31
- var commandsCollection = (firestore, channel) => collection(firestore, "users", channel.uid, "hosts", channel.hostId, "commands");
32
- var hostDoc = (firestore, channel) => doc(firestore, "users", channel.uid, "hosts", channel.hostId);
33
- //#endregion
34
- export { hostDoc as a, RUNNER_HEALTH_STATES as c, commandsCollection as i, isRunnerHealth as l, buildHostPresence as n, isExpired as o, byCreatedAt as r, toJsonObject as s, REMOTE_HOST_PROTOCOL_VERSION as t, isRunnerHealthState as u };
35
-
36
- //# sourceMappingURL=remote-host-D_BRFHcI.js.map