@mulmoclaude/core 0.2.0 → 0.2.2

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 (54) hide show
  1. package/assets/helps/collection-skills.md +52 -4
  2. package/assets/helps/feeds.md +16 -7
  3. package/assets/skills-preset/mc-manage-automations/SKILL.md +36 -2
  4. package/dist/collection/core/schema.d.ts +37 -10
  5. package/dist/collection/index.cjs +4 -3
  6. package/dist/collection/index.cjs.map +1 -1
  7. package/dist/collection/index.js +4 -4
  8. package/dist/collection/index.js.map +1 -1
  9. package/dist/collection/paths.cjs.map +1 -1
  10. package/dist/collection/paths.js.map +1 -1
  11. package/dist/collection/server/discovery.d.ts +14 -2
  12. package/dist/collection/server/index.cjs +1 -1
  13. package/dist/collection/server/index.js +1 -1
  14. package/dist/collection/server/io.d.ts +1 -1
  15. package/dist/collection-watchers/index.cjs +2 -3
  16. package/dist/collection-watchers/index.cjs.map +1 -1
  17. package/dist/collection-watchers/index.js +2 -2
  18. package/dist/collection-watchers/index.js.map +1 -1
  19. package/dist/{deriveAll-C15OpM3K.cjs → deriveAll-VRWrs3SF.cjs} +19 -5
  20. package/dist/deriveAll-VRWrs3SF.cjs.map +1 -0
  21. package/dist/{deriveAll-C6BYnpBL.js → deriveAll-vzIhhKBK.js} +14 -6
  22. package/dist/deriveAll-vzIhhKBK.js.map +1 -0
  23. package/dist/file-change/index.cjs +2 -2
  24. package/dist/file-change/index.cjs.map +1 -1
  25. package/dist/notifier/index.cjs +1 -1
  26. package/dist/notifier/index.js +1 -1
  27. package/dist/{notifier-6PjsLxLm.js → notifier-ChpY0XrY.js} +1 -1
  28. package/dist/{notifier-6PjsLxLm.js.map → notifier-ChpY0XrY.js.map} +1 -1
  29. package/dist/{notifier-lJ4v2Y6B.cjs → notifier-bS8IEeLA.cjs} +1 -2
  30. package/dist/{notifier-lJ4v2Y6B.cjs.map → notifier-bS8IEeLA.cjs.map} +1 -1
  31. package/dist/scheduler/index.cjs +2 -2
  32. package/dist/scheduler/index.cjs.map +1 -1
  33. package/dist/scheduler/index.js.map +1 -1
  34. package/dist/{server-BhIdZgqu.js → server-BPDCdvOI.js} +16 -7
  35. package/dist/server-BPDCdvOI.js.map +1 -0
  36. package/dist/{server-BjoKk2tR.cjs → server-Bego8VyU.cjs} +18 -9
  37. package/dist/server-Bego8VyU.cjs.map +1 -0
  38. package/dist/skill-bridge/index.cjs +2 -2
  39. package/dist/skill-bridge/index.cjs.map +1 -1
  40. package/dist/whisper/client.cjs +1 -1
  41. package/dist/whisper/client.cjs.map +1 -1
  42. package/dist/whisper/client.js +1 -1
  43. package/dist/whisper/client.js.map +1 -1
  44. package/dist/whisper/index.cjs +3 -3
  45. package/dist/whisper/index.cjs.map +1 -1
  46. package/dist/whisper/index.js +1 -1
  47. package/dist/whisper/index.js.map +1 -1
  48. package/dist/workspace-setup/index.js.map +1 -1
  49. package/package.json +3 -3
  50. package/dist/deriveAll-C15OpM3K.cjs.map +0 -1
  51. package/dist/deriveAll-C6BYnpBL.js.map +0 -1
  52. package/dist/server-BhIdZgqu.js.map +0 -1
  53. package/dist/server-BjoKk2tR.cjs.map +0 -1
  54. /package/dist/{chunk-CKQMccvm.cjs → rolldown-runtime-D6vf50IK.cjs} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deriveAll-VRWrs3SF.cjs","names":[],"sources":["../src/collection/core/schema.ts","../src/collection/core/derivedFormula.ts","../src/collection/core/deriveAll.ts"],"sourcesContent":["// Schema-driven collection types. A \"collection\" is a skill (under\n// .claude/skills/<slug>/) that also ships a sibling `schema.json`.\n// The host's <CollectionView> reads the schema + records and renders\n// a table/form; Claude reads SKILL.md and CRUDs the records as JSON\n// files.\n//\n// Field types for v0 — keep this list narrow and grow it only when a\n// real collection needs the new type. v0 supports flat records only;\n// nested tables / cross-collection refs / derived fields / actions are\n// deferred to follow-ups (see plans/done/feat-skill-driven-apps.md and\n// plans/done/feat-skill-driven-apps-worklog.md — historical names predate\n// the rename).\n\n/** Minimal \"this collection is a feed\" descriptor carried on the schema.\n * Deliberately narrow — the canonical collection contract stays\n * independent of the host's feeds subsystem. The host's richer retrieval\n * spec (`IngestSpec` in `server/workspace/feeds/ingestTypes.ts`)\n * `extends CollectionIngest`, so feed code reads the extra fields by\n * typing feed schemas with that subtype; collection rendering only needs\n * these three + the presence check. */\nexport interface CollectionIngest {\n kind: string;\n schedule: string;\n /** Optional time-of-day anchor for `schedule: \"daily\"` — the hour (0–23) to\n * refresh around (the host ticks hourly, so the run lands within that hour).\n * Ignored for non-daily schedules. Absent ⇒ elapsed-based daily (\"≥24 h since\n * the last run\"). NOTE: **UTC**, not local — compared via `getUTCHours()` for\n * an unambiguous, DST-free check (matching the rest of the scheduler), so\n * convert local times before writing (e.g. 07:00 JST → `atHour: 22`). */\n atHour?: number;\n /** Declarative retrievers (`rss`/`atom`/`http-json`) only — the host fetches\n * this URL on the schedule. Absent for `kind: \"agent\"`, where the agent owns\n * retrieval. */\n url?: string;\n /** `kind: \"agent\"` only: role id the scheduled hidden worker runs in. */\n role?: string;\n /** `kind: \"agent\"` only: skill-relative template path (under `templates/`)\n * whose prose tells the worker how to refresh the records. */\n template?: string;\n}\n\n/** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's\n * feeds engine dispatches on these; they live here (with the schema contract)\n * so the schema validator can enforce them. The host re-exports these from\n * `server/workspace/feeds/ingestTypes.ts`. */\nexport const INGEST_KINDS = [\"rss\", \"atom\", \"http-json\"] as const;\nexport type IngestKind = (typeof INGEST_KINDS)[number];\n\n/** The agent-performed ingest kind. Instead of a declarative fetch, the host\n * dispatches a hidden background chat (origin `system`) in `ingest.role`,\n * seeded with `ingest.template` + a summary of every record, on the\n * `ingest.schedule` cadence; the worker edits records via the collections io\n * layer. Kept separate from {@link INGEST_KINDS} (which the declarative\n * retriever registry keys on) so the schema validator can model `ingest` as a\n * discriminated union without the feeds engine gaining an \"agent\" retriever. */\nexport const AGENT_INGEST_KIND = \"agent\" as const;\nexport type AgentIngestKind = typeof AGENT_INGEST_KIND;\n\n/** Refresh cadences a Feed's `ingest.schedule` may declare. */\nexport const FEED_SCHEDULES = [\"hourly\", \"daily\", \"weekly\", \"on-demand\"] as const;\nexport type FeedSchedule = (typeof FEED_SCHEDULES)[number];\n\nexport type CollectionFieldType =\n | \"string\"\n | \"text\"\n | \"email\"\n | \"number\"\n | \"date\"\n | \"datetime\"\n | \"boolean\"\n | \"markdown\"\n | \"ref\"\n | \"money\"\n | \"enum\"\n | \"table\"\n | \"derived\"\n | \"embed\"\n // Holds a workspace-relative image path (e.g. a `data/attachments/...`\n // upload); rendered as an <img> in the detail view (not the list table —\n // a per-row fetch is too expensive at scale). Stored and edited as a\n // plain string.\n | \"image\"\n // Holds a workspace-relative file path as a plain string (e.g. an\n // `artifacts/html/<name>.html` app). Rendered as a clickable link in\n // both the list table and the detail view: HTML / SVG artifacts open\n // their rendered form in a new tab; any other path opens in the File\n // Explorer. Stored and edited as a plain string, like `image`.\n | \"file\"\n // A checkbox that is a pure PROJECTION of an `enum` field — it stores\n // nothing of its own. Checked when the enum equals `onValue`; toggling\n // writes `onValue` / `offValue` back to that enum field. Lets a \"done\"\n // checkbox front a kanban `status` field with the enum as the single\n // source of truth (no separate stored boolean to keep in sync).\n | \"toggle\";\n\n// \"feed\" collections live in the non-skill `<workspace>/feeds/` registry\n// and carry an `ingest` block; they reuse the same storage + rendering\n// as skill-backed collections but are never loaded into the agent prompt.\nexport type CollectionSource = \"user\" | \"project\" | \"feed\";\n\n/** Recurrence unit for a `spawn.every` advance. */\nexport type CollectionRecurUnit = \"day\" | \"week\" | \"month\" | \"year\";\n\n/** How a `spawn` advances the source item's `triggerField` date to\n * produce the successor's. All arithmetic is done on the civil\n * (year, month, day) triple — never by adding milliseconds — so month\n * lengths and leap years are handled correctly. */\nexport interface CollectionEvery {\n unit: CollectionRecurUnit;\n /** Number of `unit`s to advance (≥ 1). `interval: 3` + `unit: \"month\"`\n * = quarterly; `interval: 1` + `unit: \"year\"` = annual. */\n interval: number;\n /** Day-of-month anchor for `month`/`year` units. The CANONICAL day —\n * read from the rule, never re-derived from the prior concrete date,\n * so \"31st of every month\" yields 31 → 28/29 → 31 → 30 … with no\n * drift (it is clamped per-month at compute time, not stored\n * clamped). `\"last\"` always means the last day of the target month.\n * Omitted ⇒ preserve the source date's day (safe for days ≤ 28).\n * Ignored for `day`/`week` units. */\n dayOfMonth?: number | \"last\";\n}\n\n/** Field-driven recurrence: the advance interval is selected PER RECORD by\n * the value of an `enum` field (`fromField`), looked up in `map`. Lets one\n * collection mix daily / weekly / monthly obligations in a single list — the\n * host reads `record[fromField]`, finds the matching `CollectionEvery`, and\n * advances by it. `fromField` must point at a top-level `enum` field whose\n * `values` the `map` keys exactly cover (validated at discovery), and must\n * itself be carried/`set` onto the successor so the chain keeps recurring. */\nexport interface CollectionEveryFieldDriven {\n /** Top-level `enum` field whose value selects the interval. */\n fromField: string;\n /** Interval per enum value. Keys exactly cover `fromField`'s `values`;\n * each value is a literal {@link CollectionEvery}. */\n map: Record<string, CollectionEvery>;\n}\n\n/** The `every` of a `spawn`: either a single literal interval applied to\n * every record, or a per-record interval selected by an `enum` field. The\n * literal arm is what `advanceTriggerDate` consumes — the field-driven arm\n * is resolved down to one of its `map` values before the date math runs. */\nexport type CollectionSpawnEvery = CollectionEvery | CollectionEveryFieldDriven;\n\n/** Narrowing guard: true when `every` is the field-driven arm. */\nexport function isFieldDrivenEvery(every: CollectionSpawnEvery): every is CollectionEveryFieldDriven {\n return \"fromField\" in every;\n}\n\n/** Host-driven recurrence: when a record satisfies `when`, the host\n * creates the next record with a forward-advanced `triggerField` date.\n * The successor's id and contents are a pure function of (source\n * record, this rule); creation is create-if-absent, so the mechanism\n * stays convergent — observing the predicate N times writes one\n * successor. Requires the schema to declare `triggerField`. */\nexport interface CollectionSpawn {\n /** Predicate that fires the spawn (a `CollectionWhen`). Defaults to\n * \"`completionField` value ∈ `completionDoneValues`\" (i.e. spawn the\n * next instance when this one is done). */\n when?: CollectionWhen;\n /** How to advance `triggerField` from the source to the successor —\n * either a single literal interval or a per-record, field-driven map. */\n every: CollectionSpawnEvery;\n /** Record fields copied verbatim onto the successor. Fields not listed\n * here, not in `set`, and not the trigger / primary keys start\n * blank. */\n carry?: string[];\n /** Fields forced to fixed values on the successor (typically resetting\n * the status field to its pending value). */\n set?: Record<string, unknown>;\n}\n\n/** The kind of work an action kicks off. v1 ships only `\"chat\"` —\n * start a new chat in a role with a templated seed prompt. The enum\n * reserves room for a future `\"mutate\"` (status transitions) without\n * another schema-shape change. */\nexport type CollectionActionKind = \"chat\";\n\n/** Optional visibility predicate: the target (an action button or a\n * field) renders only when the open record's `field` (stringified) is\n * one of `in`. Generic and domain-free — the host evaluates it against\n * the record with no knowledge of what the field means. Absent ⇒\n * always shown. */\nexport interface CollectionWhen {\n /** Top-level record field key whose value gates visibility. */\n field: string;\n /** Allowed values; the target shows when `String(record[field])` is\n * one of these. Non-empty. */\n in: string[];\n}\n\n/** @deprecated Name retained for back-compat; use {@link CollectionWhen}.\n * Both actions and fields share the same predicate shape. */\nexport type CollectionActionWhen = CollectionWhen;\n\n/** What a custom view's capability token is allowed to do against the\n * collection's data endpoint. `read` returns enriched records (getItems\n * semantics); `write` validates-and-stores rows (putItems semantics).\n * There is deliberately no `delete` — a view can never do more than the\n * agent's own `manageCollection` tool. */\nexport type CollectionViewCapability = \"read\" | \"write\";\n\n/** A custom (LLM-authored) HTML view for a collection. The host renders\n * `file` in a sandboxed iframe over the collection's records; the view\n * reaches its data only through a slug- and capability-scoped token (see\n * `server/api/auth/viewToken.ts`). Pure data — the host holds no\n * view-specific code; meaning lives in the HTML file + this registration. */\nexport interface CollectionCustomView {\n /** Stable id; the view-mode selector key (`custom:<id>`) and the\n * capability-token clamp key. Must be a valid slug. */\n id: string;\n /** Button label in the view-mode selector (author-authored, like field\n * labels — not run through i18n). */\n label: string;\n /** Optional Material-icon name for the selector button. */\n icon?: string;\n /** Skill-relative path to the HTML file under `views/` (e.g.\n * `views/year.html`). Path-safe, must end in `.html`. */\n file: string;\n /** What the view may do with the data endpoint. Defaults to `[\"read\"]`\n * (least privilege); declare `[\"read\",\"write\"]` only for views that\n * edit records. The mint endpoint clamps any requested caps to this. */\n capabilities?: CollectionViewCapability[];\n}\n\n/** A schema-declared, per-record action rendered as a button in the\n * read-only detail view. Pure UI/behaviour directive — never stored,\n * never validated against record data. All domain specifics (label,\n * role, template) live here in the schema / skill folder, so the host\n * stays generic. */\nexport interface CollectionAction {\n /** Stable id (used in the dispatch route + testids). */\n id: string;\n /** Button text (English, like field labels). */\n label: string;\n /** Material-icon name shown on the button. */\n icon?: string;\n /** What the action does. v1: `\"chat\"`. */\n kind: CollectionActionKind;\n /** `kind: \"chat\"`: the role id the new chat runs in. */\n role: string;\n /** `kind: \"chat\"`: skill-relative path to the template file whose\n * text becomes the seed prompt body (e.g. `templates/invoice.md`). */\n template: string;\n /** Optional visibility predicate; the button renders only when the\n * open record matches (see CollectionWhen). Absent ⇒ always\n * shown. */\n when?: CollectionWhen;\n}\n\nexport interface CollectionFieldSpec {\n type: CollectionFieldType;\n label: string;\n /** True for the field whose value is the record's filename (no\n * separate auto-id). Exactly one field per schema may set this. */\n primary?: boolean;\n required?: boolean;\n /** When `type === \"ref\"` or `type === \"embed\"`: the slug of the\n * target collection. For `ref` the record stores the target\n * item's primary-key slug and the host renders a clickable link\n * + dropdown picker. For `embed` the host pulls a *fixed* record\n * (see `id`) from the target and renders its fields read-only in\n * the detail view. Required for both; ignored on every other\n * type. */\n to?: string;\n /** When `type === \"embed\"`: the primary-key value of the fixed\n * record to pull from the `to` collection (e.g. `me` for the\n * singleton mc-profile). Nothing is stored on this record — the\n * embed is a display-only directive resolved at render time, so\n * it never appears in the list table or the edit form. Required\n * when type is `embed`; ignored on every other type. */\n id?: string;\n /** When `type === \"money\"` (or `type === \"derived\"` with\n * `display: \"money\"`): a literal ISO 4217 currency code passed to\n * `Intl.NumberFormat` for display — fixed for every record. The\n * stored value is always a plain decimal number; currency is\n * presentation only. Mutually substitutable with `currencyField`:\n * a money field must declare at least one of the two. */\n currency?: string;\n /** When `type === \"money\"` (or `type === \"derived\"` with\n * `display: \"money\"`): the name of a sibling record field whose\n * value holds the ISO 4217 code, letting currency vary per record\n * (e.g. an invoice's `currency` enum). The renderer reads\n * `record[currencyField]` and falls back to the literal `currency`\n * (then \"USD\") when the field is absent or empty. Resolved against\n * the top-level record even for money sub-fields inside a table. */\n currencyField?: string;\n /** When `type === \"enum\"`: the closed set of allowed string\n * values. The form renders a `<select>` populated from this\n * list; storage is a plain string. Required when type is\n * `enum`; ignored on every other type. */\n values?: readonly string[];\n /** When `type === \"table\"`: the sub-schema for each row (a flat\n * record of non-table / non-derived field specs). Required when\n * type is `table`. v0 disallows nested tables and derived\n * columns to keep the editor + evaluator simple. */\n of?: Record<string, CollectionFieldSpec>;\n /** When `type === \"derived\"`: a tiny expression evaluated against\n * the record. Supports `+ - * /`, parens, identifier refs to\n * top-level fields, `sum(tableField[].col)`, and\n * `sum(tableField[].col * tableField[].col)`. See\n * `src/utils/collections/derivedFormula.ts`. Required when type\n * is `derived`. */\n formula?: string;\n /** When `type === \"derived\"`: an inner field type the computed\n * value should be rendered as (e.g. `\"money\"` so $1,234.56 is\n * formatted). Defaults to `\"number\"`. */\n display?: CollectionFieldType;\n /** Optional visibility predicate: this field renders only when the\n * record matches (e.g. hide a `rating` field until `visited` is\n * `true` via `{ field: \"visited\", in: [\"true\"] }`). Applies to the\n * list cell (blank when hidden), the edit form (hidden live as the\n * gating field changes), and the detail view. Purely presentational\n * — a hidden field's stored value is never cleared. `when.field`\n * must name another top-level field. Absent ⇒ always shown. Only\n * honoured on top-level fields, not inside a `table`'s `of`. */\n when?: CollectionWhen;\n /** When `type === \"toggle\"`: the name of the top-level `enum` field this\n * checkbox projects. The toggle stores nothing itself — it reads and\n * writes this field. Required when type is `toggle`; ignored otherwise.\n * Must name a real `enum` field. */\n field?: string;\n /** When `type === \"toggle\"`: the enum value that means \"checked\". The\n * box is checked when the projected `field` equals this; checking writes\n * it. Required when type is `toggle`; must be one of the enum's `values`. */\n onValue?: string;\n /** When `type === \"toggle\"`: the enum value written when the box is\n * unchecked. Required when type is `toggle`; must be one of the enum's\n * `values`. */\n offValue?: string;\n}\n\nexport interface CollectionSchema {\n /** Human-facing collection name (sidebar, header). */\n title: string;\n /** Material-icon name shown next to the title. */\n icon: string;\n /** Workspace-relative folder holding one-JSON-per-record. Validated\n * to live under the workspace root at load time. */\n dataPath: string;\n /** Field name whose value doubles as the record's filename. */\n primaryKey: string;\n /** When set, the collection is a singleton: at most one record,\n * whose primary key is fixed to this value (e.g. `me` for the\n * business profile). The host pre-fills + locks the create form's\n * primary key and hides Add once the record exists. */\n singleton?: string;\n /** Ordered map: insertion order = column order in the table view. */\n fields: Record<string, CollectionFieldSpec>;\n /** Optional per-record actions rendered as buttons in the detail\n * view (e.g. \"Generate PDF\"). Order = button order. */\n actions?: CollectionAction[];\n /** Optional collection-level actions rendered as buttons in the\n * collection header (e.g. \"Extend the course\"). Unlike `actions`,\n * these carry no record context: the seed prompt injects a compact\n * progress summary of every record instead. The `when` predicate is\n * not evaluated (there is no record to gate on). Order = button order. */\n collectionActions?: CollectionAction[];\n /** Name of the field whose value marks an item as \"done\". When set,\n * a notification fires on item create (unless the item is born done)\n * and clears when the field's value transitions into\n * `completionDoneValues`. Must name a real field in `fields`. */\n completionField?: string;\n /** The set of values for `completionField` that count as \"done\"\n * (e.g. `[\"Done\"]` for a todo status field, `[\"paid\"]` for an\n * invoice). Non-empty. Compared as strings. */\n completionDoneValues?: readonly string[];\n /** Name of the field whose value is shown as the human-readable\n * label in a completion notification's title (e.g. a `name` field,\n * so the bell reads `Contacts: Jane Doe` instead of the opaque\n * primaryKey). Must name a real field in `fields`. When unset — or\n * when the record's value for it is empty — the title falls back to\n * the record's primaryKey value. Display-only; never stored. */\n displayField?: string;\n /** Name of a `date` field that gates this item's completion\n * notification: the bell is suppressed until the clock reaches that\n * date (compared at day-granularity in the server's local timezone),\n * instead of firing on create. Requires `completionField` /\n * `completionDoneValues` (the bell still clears via the done value).\n * Must name a real `date` field. Absent ⇒ fire on create, as before. */\n triggerField?: string;\n /** Lead time in whole days: fire the bell this many days BEFORE\n * `triggerField` (so `10` shows the reminder 10 days early). The lead\n * is applied at fire time, not stored, so it composes with `spawn` —\n * every recurred cycle fires the same number of days before its own\n * trigger. Non-negative integer; requires `triggerField`. Default 0\n * (fire on the trigger date). */\n triggerLeadDays?: number;\n /** Host-driven recurrence. When set, requires `triggerField`. See\n * {@link CollectionSpawn}. */\n spawn?: CollectionSpawn;\n /** Name of a `date` field that anchors the optional calendar view: a\n * month grid where each record lands on the day cell matching this\n * field's value. When unset, the calendar toggle still appears if the\n * schema has any `date` field (the first one, in declaration order, is\n * used by default and is switchable in-view). Set this to pin a specific\n * anchor. Must name a real `date` field. */\n calendarField?: string;\n /** Name of a second `date` field marking the END of a multi-day span on\n * the calendar: the record renders from `calendarField` through this\n * date inclusive. Requires `calendarField`. Must name a real `date`\n * field. Absent ⇒ single-day placement. */\n calendarEndField?: string;\n /** Name of a string field holding a free-form time or time-range\n * (e.g. \"14:00-17:00\", \"17:00-\", \"16:30\") that places records on the\n * calendar's day (time-allocation) view. Consulted only when the calendar\n * date fields are date-only. Requires `calendarField`. */\n calendarTimeField?: string;\n /** Name of an `enum` field that groups records into columns on the\n * optional Kanban board: each record lands in the column matching its\n * value, with empty/unknown values collected in an \"Uncategorized\"\n * column. When unset, the Kanban toggle still appears if the schema has\n * any `enum` field (the first one, in declaration order, is used by\n * default and is switchable in-view). Set this to pin a specific group\n * field. Must name a real `enum` field. */\n kanbanField?: string;\n /** Optional custom (LLM-authored) HTML views, each rendered in a\n * sandboxed iframe over the records. Absent ⇒ only the built-in\n * field-derived views (table / calendar / kanban / dashboard). See\n * {@link CollectionCustomView}. */\n views?: CollectionCustomView[];\n /** Optional predicate that gates the completion bell: when set, the bell\n * fires only for records whose `String(record[notifyWhen.field])` is one\n * of `notifyWhen.in` (e.g. notify only `high`/`urgent` priority todos).\n * Reuses the `when` predicate shape. Requires `completionField` — it\n * narrows that bell rather than introducing a second one. The bell still\n * clears on done / delete / when the predicate stops matching. Absent ⇒\n * notify for every open record (the prior behaviour). `notifyWhen.field`\n * must name a real top-level field. */\n notifyWhen?: CollectionWhen;\n /** Optional scheduled-retrieval config. When present, the host refreshes\n * this collection on `ingest.schedule`. Two flavours: a declarative Feed\n * (`kind: rss/atom/http-json`) periodically fetches `ingest.url`, maps the\n * response into records, and upserts them by `primaryKey` — only feeds\n * discovered from the `<workspace>/feeds/` registry carry this. Or\n * `kind: \"agent\"`, valid on any (incl. skill-backed) collection: the host\n * dispatches a hidden background worker in `ingest.role` seeded with\n * `ingest.template`, and the worker edits the records itself. The host's\n * feeds subsystem narrows this to its richer `IngestSpec`. */\n ingest?: CollectionIngest;\n}\n\nexport interface CollectionSummary {\n slug: string;\n title: string;\n icon: string;\n source: CollectionSource;\n}\n\nexport interface CollectionDetail extends CollectionSummary {\n schema: CollectionSchema;\n}\n\nexport type CollectionItem = Record<string, unknown>;\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","// 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\";\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}\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 refs[key] = typeof slug === \"string\" ? (refRecords[field.to]?.[slug] ?? null) : null;\n }\n return refs;\n}\n\n/** Evaluate every `derived` field against `base`, saturating so a\n * derived field can read another derived field computed in an earlier\n * pass (`subtotal → tax → total` converges in ≤ field-count passes).\n * Cycles can't loop forever — passes are bounded by the number of\n * derived fields and the loop breaks as soon as a pass changes\n * nothing. Failed formulas stay ABSENT (the UI renders them as\n * em-dash). Returns a copy; `base` is never mutated.\n *\n * Derived 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) derived 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 derivedKeys = new Set(Object.keys(schema.fields).filter((key) => schema.fields[key]?.type === \"derived\"));\n const enriched: DerivableRecord = Object.fromEntries(Object.entries(base).filter(([key]) => !derivedKeys.has(key)));\n const refs = resolveRowRefs(schema, base, refRecords);\n const maxPasses = Object.values(schema.fields).filter((field) => field.type === \"derived\").length;\n for (let pass = 0; pass < maxPasses; pass++) {\n let mutated = false;\n for (const [key, field] of Object.entries(schema.fields)) {\n if (field.type !== \"derived\" || !field.formula) continue;\n const next = evaluateDerived(field.formula, { record: enriched, refs });\n if (next !== null && enriched[key] !== next) {\n enriched[key] = next;\n mutated = true;\n }\n }\n if (!mutated) break;\n }\n return enriched;\n}\n"],"mappings":"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"}
@@ -1,13 +1,21 @@
1
1
  //#region src/collection/core/schema.ts
2
- /** Retriever kinds a Feed's `ingest.kind` may declare. The host's feeds engine
3
- * dispatches on these; they live here (with the schema contract) so the schema
4
- * validator can enforce them. The host re-exports these from
2
+ /** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's
3
+ * feeds engine dispatches on these; they live here (with the schema contract)
4
+ * so the schema validator can enforce them. The host re-exports these from
5
5
  * `server/workspace/feeds/ingestTypes.ts`. */
6
6
  var INGEST_KINDS = [
7
7
  "rss",
8
8
  "atom",
9
9
  "http-json"
10
10
  ];
11
+ /** The agent-performed ingest kind. Instead of a declarative fetch, the host
12
+ * dispatches a hidden background chat (origin `system`) in `ingest.role`,
13
+ * seeded with `ingest.template` + a summary of every record, on the
14
+ * `ingest.schedule` cadence; the worker edits records via the collections io
15
+ * layer. Kept separate from {@link INGEST_KINDS} (which the declarative
16
+ * retriever registry keys on) so the schema validator can model `ingest` as a
17
+ * discriminated union without the feeds engine gaining an "agent" retriever. */
18
+ var AGENT_INGEST_KIND = "agent";
11
19
  /** Refresh cadences a Feed's `ingest.schedule` may declare. */
12
20
  var FEED_SCHEDULES = [
13
21
  "hourly",
@@ -39,7 +47,7 @@ function evaluateDerived(formula, ctx) {
39
47
  const value = evaluate(ast, ctx);
40
48
  return Number.isFinite(value) ? value : null;
41
49
  }
42
- var SINGLE_CHAR_PUNCT = new Set([
50
+ var SINGLE_CHAR_PUNCT = /* @__PURE__ */ new Set([
43
51
  "(",
44
52
  ")",
45
53
  "+",
@@ -359,6 +367,6 @@ function deriveAll(schema, base, refRecords) {
359
367
  return enriched;
360
368
  }
361
369
  //#endregion
362
- export { INGEST_KINDS as a, FEED_SCHEDULES as i, resolveRowRefs as n, isFieldDrivenEvery as o, evaluateDerived as r, deriveAll as t };
370
+ export { FEED_SCHEDULES as a, AGENT_INGEST_KIND as i, resolveRowRefs as n, INGEST_KINDS as o, evaluateDerived as r, isFieldDrivenEvery as s, deriveAll as t };
363
371
 
364
- //# sourceMappingURL=deriveAll-C6BYnpBL.js.map
372
+ //# sourceMappingURL=deriveAll-vzIhhKBK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deriveAll-vzIhhKBK.js","names":[],"sources":["../src/collection/core/schema.ts","../src/collection/core/derivedFormula.ts","../src/collection/core/deriveAll.ts"],"sourcesContent":["// Schema-driven collection types. A \"collection\" is a skill (under\n// .claude/skills/<slug>/) that also ships a sibling `schema.json`.\n// The host's <CollectionView> reads the schema + records and renders\n// a table/form; Claude reads SKILL.md and CRUDs the records as JSON\n// files.\n//\n// Field types for v0 — keep this list narrow and grow it only when a\n// real collection needs the new type. v0 supports flat records only;\n// nested tables / cross-collection refs / derived fields / actions are\n// deferred to follow-ups (see plans/done/feat-skill-driven-apps.md and\n// plans/done/feat-skill-driven-apps-worklog.md — historical names predate\n// the rename).\n\n/** Minimal \"this collection is a feed\" descriptor carried on the schema.\n * Deliberately narrow — the canonical collection contract stays\n * independent of the host's feeds subsystem. The host's richer retrieval\n * spec (`IngestSpec` in `server/workspace/feeds/ingestTypes.ts`)\n * `extends CollectionIngest`, so feed code reads the extra fields by\n * typing feed schemas with that subtype; collection rendering only needs\n * these three + the presence check. */\nexport interface CollectionIngest {\n kind: string;\n schedule: string;\n /** Optional time-of-day anchor for `schedule: \"daily\"` — the hour (0–23) to\n * refresh around (the host ticks hourly, so the run lands within that hour).\n * Ignored for non-daily schedules. Absent ⇒ elapsed-based daily (\"≥24 h since\n * the last run\"). NOTE: **UTC**, not local — compared via `getUTCHours()` for\n * an unambiguous, DST-free check (matching the rest of the scheduler), so\n * convert local times before writing (e.g. 07:00 JST → `atHour: 22`). */\n atHour?: number;\n /** Declarative retrievers (`rss`/`atom`/`http-json`) only — the host fetches\n * this URL on the schedule. Absent for `kind: \"agent\"`, where the agent owns\n * retrieval. */\n url?: string;\n /** `kind: \"agent\"` only: role id the scheduled hidden worker runs in. */\n role?: string;\n /** `kind: \"agent\"` only: skill-relative template path (under `templates/`)\n * whose prose tells the worker how to refresh the records. */\n template?: string;\n}\n\n/** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's\n * feeds engine dispatches on these; they live here (with the schema contract)\n * so the schema validator can enforce them. The host re-exports these from\n * `server/workspace/feeds/ingestTypes.ts`. */\nexport const INGEST_KINDS = [\"rss\", \"atom\", \"http-json\"] as const;\nexport type IngestKind = (typeof INGEST_KINDS)[number];\n\n/** The agent-performed ingest kind. Instead of a declarative fetch, the host\n * dispatches a hidden background chat (origin `system`) in `ingest.role`,\n * seeded with `ingest.template` + a summary of every record, on the\n * `ingest.schedule` cadence; the worker edits records via the collections io\n * layer. Kept separate from {@link INGEST_KINDS} (which the declarative\n * retriever registry keys on) so the schema validator can model `ingest` as a\n * discriminated union without the feeds engine gaining an \"agent\" retriever. */\nexport const AGENT_INGEST_KIND = \"agent\" as const;\nexport type AgentIngestKind = typeof AGENT_INGEST_KIND;\n\n/** Refresh cadences a Feed's `ingest.schedule` may declare. */\nexport const FEED_SCHEDULES = [\"hourly\", \"daily\", \"weekly\", \"on-demand\"] as const;\nexport type FeedSchedule = (typeof FEED_SCHEDULES)[number];\n\nexport type CollectionFieldType =\n | \"string\"\n | \"text\"\n | \"email\"\n | \"number\"\n | \"date\"\n | \"datetime\"\n | \"boolean\"\n | \"markdown\"\n | \"ref\"\n | \"money\"\n | \"enum\"\n | \"table\"\n | \"derived\"\n | \"embed\"\n // Holds a workspace-relative image path (e.g. a `data/attachments/...`\n // upload); rendered as an <img> in the detail view (not the list table —\n // a per-row fetch is too expensive at scale). Stored and edited as a\n // plain string.\n | \"image\"\n // Holds a workspace-relative file path as a plain string (e.g. an\n // `artifacts/html/<name>.html` app). Rendered as a clickable link in\n // both the list table and the detail view: HTML / SVG artifacts open\n // their rendered form in a new tab; any other path opens in the File\n // Explorer. Stored and edited as a plain string, like `image`.\n | \"file\"\n // A checkbox that is a pure PROJECTION of an `enum` field — it stores\n // nothing of its own. Checked when the enum equals `onValue`; toggling\n // writes `onValue` / `offValue` back to that enum field. Lets a \"done\"\n // checkbox front a kanban `status` field with the enum as the single\n // source of truth (no separate stored boolean to keep in sync).\n | \"toggle\";\n\n// \"feed\" collections live in the non-skill `<workspace>/feeds/` registry\n// and carry an `ingest` block; they reuse the same storage + rendering\n// as skill-backed collections but are never loaded into the agent prompt.\nexport type CollectionSource = \"user\" | \"project\" | \"feed\";\n\n/** Recurrence unit for a `spawn.every` advance. */\nexport type CollectionRecurUnit = \"day\" | \"week\" | \"month\" | \"year\";\n\n/** How a `spawn` advances the source item's `triggerField` date to\n * produce the successor's. All arithmetic is done on the civil\n * (year, month, day) triple — never by adding milliseconds — so month\n * lengths and leap years are handled correctly. */\nexport interface CollectionEvery {\n unit: CollectionRecurUnit;\n /** Number of `unit`s to advance (≥ 1). `interval: 3` + `unit: \"month\"`\n * = quarterly; `interval: 1` + `unit: \"year\"` = annual. */\n interval: number;\n /** Day-of-month anchor for `month`/`year` units. The CANONICAL day —\n * read from the rule, never re-derived from the prior concrete date,\n * so \"31st of every month\" yields 31 → 28/29 → 31 → 30 … with no\n * drift (it is clamped per-month at compute time, not stored\n * clamped). `\"last\"` always means the last day of the target month.\n * Omitted ⇒ preserve the source date's day (safe for days ≤ 28).\n * Ignored for `day`/`week` units. */\n dayOfMonth?: number | \"last\";\n}\n\n/** Field-driven recurrence: the advance interval is selected PER RECORD by\n * the value of an `enum` field (`fromField`), looked up in `map`. Lets one\n * collection mix daily / weekly / monthly obligations in a single list — the\n * host reads `record[fromField]`, finds the matching `CollectionEvery`, and\n * advances by it. `fromField` must point at a top-level `enum` field whose\n * `values` the `map` keys exactly cover (validated at discovery), and must\n * itself be carried/`set` onto the successor so the chain keeps recurring. */\nexport interface CollectionEveryFieldDriven {\n /** Top-level `enum` field whose value selects the interval. */\n fromField: string;\n /** Interval per enum value. Keys exactly cover `fromField`'s `values`;\n * each value is a literal {@link CollectionEvery}. */\n map: Record<string, CollectionEvery>;\n}\n\n/** The `every` of a `spawn`: either a single literal interval applied to\n * every record, or a per-record interval selected by an `enum` field. The\n * literal arm is what `advanceTriggerDate` consumes — the field-driven arm\n * is resolved down to one of its `map` values before the date math runs. */\nexport type CollectionSpawnEvery = CollectionEvery | CollectionEveryFieldDriven;\n\n/** Narrowing guard: true when `every` is the field-driven arm. */\nexport function isFieldDrivenEvery(every: CollectionSpawnEvery): every is CollectionEveryFieldDriven {\n return \"fromField\" in every;\n}\n\n/** Host-driven recurrence: when a record satisfies `when`, the host\n * creates the next record with a forward-advanced `triggerField` date.\n * The successor's id and contents are a pure function of (source\n * record, this rule); creation is create-if-absent, so the mechanism\n * stays convergent — observing the predicate N times writes one\n * successor. Requires the schema to declare `triggerField`. */\nexport interface CollectionSpawn {\n /** Predicate that fires the spawn (a `CollectionWhen`). Defaults to\n * \"`completionField` value ∈ `completionDoneValues`\" (i.e. spawn the\n * next instance when this one is done). */\n when?: CollectionWhen;\n /** How to advance `triggerField` from the source to the successor —\n * either a single literal interval or a per-record, field-driven map. */\n every: CollectionSpawnEvery;\n /** Record fields copied verbatim onto the successor. Fields not listed\n * here, not in `set`, and not the trigger / primary keys start\n * blank. */\n carry?: string[];\n /** Fields forced to fixed values on the successor (typically resetting\n * the status field to its pending value). */\n set?: Record<string, unknown>;\n}\n\n/** The kind of work an action kicks off. v1 ships only `\"chat\"` —\n * start a new chat in a role with a templated seed prompt. The enum\n * reserves room for a future `\"mutate\"` (status transitions) without\n * another schema-shape change. */\nexport type CollectionActionKind = \"chat\";\n\n/** Optional visibility predicate: the target (an action button or a\n * field) renders only when the open record's `field` (stringified) is\n * one of `in`. Generic and domain-free — the host evaluates it against\n * the record with no knowledge of what the field means. Absent ⇒\n * always shown. */\nexport interface CollectionWhen {\n /** Top-level record field key whose value gates visibility. */\n field: string;\n /** Allowed values; the target shows when `String(record[field])` is\n * one of these. Non-empty. */\n in: string[];\n}\n\n/** @deprecated Name retained for back-compat; use {@link CollectionWhen}.\n * Both actions and fields share the same predicate shape. */\nexport type CollectionActionWhen = CollectionWhen;\n\n/** What a custom view's capability token is allowed to do against the\n * collection's data endpoint. `read` returns enriched records (getItems\n * semantics); `write` validates-and-stores rows (putItems semantics).\n * There is deliberately no `delete` — a view can never do more than the\n * agent's own `manageCollection` tool. */\nexport type CollectionViewCapability = \"read\" | \"write\";\n\n/** A custom (LLM-authored) HTML view for a collection. The host renders\n * `file` in a sandboxed iframe over the collection's records; the view\n * reaches its data only through a slug- and capability-scoped token (see\n * `server/api/auth/viewToken.ts`). Pure data — the host holds no\n * view-specific code; meaning lives in the HTML file + this registration. */\nexport interface CollectionCustomView {\n /** Stable id; the view-mode selector key (`custom:<id>`) and the\n * capability-token clamp key. Must be a valid slug. */\n id: string;\n /** Button label in the view-mode selector (author-authored, like field\n * labels — not run through i18n). */\n label: string;\n /** Optional Material-icon name for the selector button. */\n icon?: string;\n /** Skill-relative path to the HTML file under `views/` (e.g.\n * `views/year.html`). Path-safe, must end in `.html`. */\n file: string;\n /** What the view may do with the data endpoint. Defaults to `[\"read\"]`\n * (least privilege); declare `[\"read\",\"write\"]` only for views that\n * edit records. The mint endpoint clamps any requested caps to this. */\n capabilities?: CollectionViewCapability[];\n}\n\n/** A schema-declared, per-record action rendered as a button in the\n * read-only detail view. Pure UI/behaviour directive — never stored,\n * never validated against record data. All domain specifics (label,\n * role, template) live here in the schema / skill folder, so the host\n * stays generic. */\nexport interface CollectionAction {\n /** Stable id (used in the dispatch route + testids). */\n id: string;\n /** Button text (English, like field labels). */\n label: string;\n /** Material-icon name shown on the button. */\n icon?: string;\n /** What the action does. v1: `\"chat\"`. */\n kind: CollectionActionKind;\n /** `kind: \"chat\"`: the role id the new chat runs in. */\n role: string;\n /** `kind: \"chat\"`: skill-relative path to the template file whose\n * text becomes the seed prompt body (e.g. `templates/invoice.md`). */\n template: string;\n /** Optional visibility predicate; the button renders only when the\n * open record matches (see CollectionWhen). Absent ⇒ always\n * shown. */\n when?: CollectionWhen;\n}\n\nexport interface CollectionFieldSpec {\n type: CollectionFieldType;\n label: string;\n /** True for the field whose value is the record's filename (no\n * separate auto-id). Exactly one field per schema may set this. */\n primary?: boolean;\n required?: boolean;\n /** When `type === \"ref\"` or `type === \"embed\"`: the slug of the\n * target collection. For `ref` the record stores the target\n * item's primary-key slug and the host renders a clickable link\n * + dropdown picker. For `embed` the host pulls a *fixed* record\n * (see `id`) from the target and renders its fields read-only in\n * the detail view. Required for both; ignored on every other\n * type. */\n to?: string;\n /** When `type === \"embed\"`: the primary-key value of the fixed\n * record to pull from the `to` collection (e.g. `me` for the\n * singleton mc-profile). Nothing is stored on this record — the\n * embed is a display-only directive resolved at render time, so\n * it never appears in the list table or the edit form. Required\n * when type is `embed`; ignored on every other type. */\n id?: string;\n /** When `type === \"money\"` (or `type === \"derived\"` with\n * `display: \"money\"`): a literal ISO 4217 currency code passed to\n * `Intl.NumberFormat` for display — fixed for every record. The\n * stored value is always a plain decimal number; currency is\n * presentation only. Mutually substitutable with `currencyField`:\n * a money field must declare at least one of the two. */\n currency?: string;\n /** When `type === \"money\"` (or `type === \"derived\"` with\n * `display: \"money\"`): the name of a sibling record field whose\n * value holds the ISO 4217 code, letting currency vary per record\n * (e.g. an invoice's `currency` enum). The renderer reads\n * `record[currencyField]` and falls back to the literal `currency`\n * (then \"USD\") when the field is absent or empty. Resolved against\n * the top-level record even for money sub-fields inside a table. */\n currencyField?: string;\n /** When `type === \"enum\"`: the closed set of allowed string\n * values. The form renders a `<select>` populated from this\n * list; storage is a plain string. Required when type is\n * `enum`; ignored on every other type. */\n values?: readonly string[];\n /** When `type === \"table\"`: the sub-schema for each row (a flat\n * record of non-table / non-derived field specs). Required when\n * type is `table`. v0 disallows nested tables and derived\n * columns to keep the editor + evaluator simple. */\n of?: Record<string, CollectionFieldSpec>;\n /** When `type === \"derived\"`: a tiny expression evaluated against\n * the record. Supports `+ - * /`, parens, identifier refs to\n * top-level fields, `sum(tableField[].col)`, and\n * `sum(tableField[].col * tableField[].col)`. See\n * `src/utils/collections/derivedFormula.ts`. Required when type\n * is `derived`. */\n formula?: string;\n /** When `type === \"derived\"`: an inner field type the computed\n * value should be rendered as (e.g. `\"money\"` so $1,234.56 is\n * formatted). Defaults to `\"number\"`. */\n display?: CollectionFieldType;\n /** Optional visibility predicate: this field renders only when the\n * record matches (e.g. hide a `rating` field until `visited` is\n * `true` via `{ field: \"visited\", in: [\"true\"] }`). Applies to the\n * list cell (blank when hidden), the edit form (hidden live as the\n * gating field changes), and the detail view. Purely presentational\n * — a hidden field's stored value is never cleared. `when.field`\n * must name another top-level field. Absent ⇒ always shown. Only\n * honoured on top-level fields, not inside a `table`'s `of`. */\n when?: CollectionWhen;\n /** When `type === \"toggle\"`: the name of the top-level `enum` field this\n * checkbox projects. The toggle stores nothing itself — it reads and\n * writes this field. Required when type is `toggle`; ignored otherwise.\n * Must name a real `enum` field. */\n field?: string;\n /** When `type === \"toggle\"`: the enum value that means \"checked\". The\n * box is checked when the projected `field` equals this; checking writes\n * it. Required when type is `toggle`; must be one of the enum's `values`. */\n onValue?: string;\n /** When `type === \"toggle\"`: the enum value written when the box is\n * unchecked. Required when type is `toggle`; must be one of the enum's\n * `values`. */\n offValue?: string;\n}\n\nexport interface CollectionSchema {\n /** Human-facing collection name (sidebar, header). */\n title: string;\n /** Material-icon name shown next to the title. */\n icon: string;\n /** Workspace-relative folder holding one-JSON-per-record. Validated\n * to live under the workspace root at load time. */\n dataPath: string;\n /** Field name whose value doubles as the record's filename. */\n primaryKey: string;\n /** When set, the collection is a singleton: at most one record,\n * whose primary key is fixed to this value (e.g. `me` for the\n * business profile). The host pre-fills + locks the create form's\n * primary key and hides Add once the record exists. */\n singleton?: string;\n /** Ordered map: insertion order = column order in the table view. */\n fields: Record<string, CollectionFieldSpec>;\n /** Optional per-record actions rendered as buttons in the detail\n * view (e.g. \"Generate PDF\"). Order = button order. */\n actions?: CollectionAction[];\n /** Optional collection-level actions rendered as buttons in the\n * collection header (e.g. \"Extend the course\"). Unlike `actions`,\n * these carry no record context: the seed prompt injects a compact\n * progress summary of every record instead. The `when` predicate is\n * not evaluated (there is no record to gate on). Order = button order. */\n collectionActions?: CollectionAction[];\n /** Name of the field whose value marks an item as \"done\". When set,\n * a notification fires on item create (unless the item is born done)\n * and clears when the field's value transitions into\n * `completionDoneValues`. Must name a real field in `fields`. */\n completionField?: string;\n /** The set of values for `completionField` that count as \"done\"\n * (e.g. `[\"Done\"]` for a todo status field, `[\"paid\"]` for an\n * invoice). Non-empty. Compared as strings. */\n completionDoneValues?: readonly string[];\n /** Name of the field whose value is shown as the human-readable\n * label in a completion notification's title (e.g. a `name` field,\n * so the bell reads `Contacts: Jane Doe` instead of the opaque\n * primaryKey). Must name a real field in `fields`. When unset — or\n * when the record's value for it is empty — the title falls back to\n * the record's primaryKey value. Display-only; never stored. */\n displayField?: string;\n /** Name of a `date` field that gates this item's completion\n * notification: the bell is suppressed until the clock reaches that\n * date (compared at day-granularity in the server's local timezone),\n * instead of firing on create. Requires `completionField` /\n * `completionDoneValues` (the bell still clears via the done value).\n * Must name a real `date` field. Absent ⇒ fire on create, as before. */\n triggerField?: string;\n /** Lead time in whole days: fire the bell this many days BEFORE\n * `triggerField` (so `10` shows the reminder 10 days early). The lead\n * is applied at fire time, not stored, so it composes with `spawn` —\n * every recurred cycle fires the same number of days before its own\n * trigger. Non-negative integer; requires `triggerField`. Default 0\n * (fire on the trigger date). */\n triggerLeadDays?: number;\n /** Host-driven recurrence. When set, requires `triggerField`. See\n * {@link CollectionSpawn}. */\n spawn?: CollectionSpawn;\n /** Name of a `date` field that anchors the optional calendar view: a\n * month grid where each record lands on the day cell matching this\n * field's value. When unset, the calendar toggle still appears if the\n * schema has any `date` field (the first one, in declaration order, is\n * used by default and is switchable in-view). Set this to pin a specific\n * anchor. Must name a real `date` field. */\n calendarField?: string;\n /** Name of a second `date` field marking the END of a multi-day span on\n * the calendar: the record renders from `calendarField` through this\n * date inclusive. Requires `calendarField`. Must name a real `date`\n * field. Absent ⇒ single-day placement. */\n calendarEndField?: string;\n /** Name of a string field holding a free-form time or time-range\n * (e.g. \"14:00-17:00\", \"17:00-\", \"16:30\") that places records on the\n * calendar's day (time-allocation) view. Consulted only when the calendar\n * date fields are date-only. Requires `calendarField`. */\n calendarTimeField?: string;\n /** Name of an `enum` field that groups records into columns on the\n * optional Kanban board: each record lands in the column matching its\n * value, with empty/unknown values collected in an \"Uncategorized\"\n * column. When unset, the Kanban toggle still appears if the schema has\n * any `enum` field (the first one, in declaration order, is used by\n * default and is switchable in-view). Set this to pin a specific group\n * field. Must name a real `enum` field. */\n kanbanField?: string;\n /** Optional custom (LLM-authored) HTML views, each rendered in a\n * sandboxed iframe over the records. Absent ⇒ only the built-in\n * field-derived views (table / calendar / kanban / dashboard). See\n * {@link CollectionCustomView}. */\n views?: CollectionCustomView[];\n /** Optional predicate that gates the completion bell: when set, the bell\n * fires only for records whose `String(record[notifyWhen.field])` is one\n * of `notifyWhen.in` (e.g. notify only `high`/`urgent` priority todos).\n * Reuses the `when` predicate shape. Requires `completionField` — it\n * narrows that bell rather than introducing a second one. The bell still\n * clears on done / delete / when the predicate stops matching. Absent ⇒\n * notify for every open record (the prior behaviour). `notifyWhen.field`\n * must name a real top-level field. */\n notifyWhen?: CollectionWhen;\n /** Optional scheduled-retrieval config. When present, the host refreshes\n * this collection on `ingest.schedule`. Two flavours: a declarative Feed\n * (`kind: rss/atom/http-json`) periodically fetches `ingest.url`, maps the\n * response into records, and upserts them by `primaryKey` — only feeds\n * discovered from the `<workspace>/feeds/` registry carry this. Or\n * `kind: \"agent\"`, valid on any (incl. skill-backed) collection: the host\n * dispatches a hidden background worker in `ingest.role` seeded with\n * `ingest.template`, and the worker edits the records itself. The host's\n * feeds subsystem narrows this to its richer `IngestSpec`. */\n ingest?: CollectionIngest;\n}\n\nexport interface CollectionSummary {\n slug: string;\n title: string;\n icon: string;\n source: CollectionSource;\n}\n\nexport interface CollectionDetail extends CollectionSummary {\n schema: CollectionSchema;\n}\n\nexport type CollectionItem = Record<string, unknown>;\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","// 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\";\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}\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 refs[key] = typeof slug === \"string\" ? (refRecords[field.to]?.[slug] ?? null) : null;\n }\n return refs;\n}\n\n/** Evaluate every `derived` field against `base`, saturating so a\n * derived field can read another derived field computed in an earlier\n * pass (`subtotal → tax → total` converges in ≤ field-count passes).\n * Cycles can't loop forever — passes are bounded by the number of\n * derived fields and the loop breaks as soon as a pass changes\n * nothing. Failed formulas stay ABSENT (the UI renders them as\n * em-dash). Returns a copy; `base` is never mutated.\n *\n * Derived 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) derived 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 derivedKeys = new Set(Object.keys(schema.fields).filter((key) => schema.fields[key]?.type === \"derived\"));\n const enriched: DerivableRecord = Object.fromEntries(Object.entries(base).filter(([key]) => !derivedKeys.has(key)));\n const refs = resolveRowRefs(schema, base, refRecords);\n const maxPasses = Object.values(schema.fields).filter((field) => field.type === \"derived\").length;\n for (let pass = 0; pass < maxPasses; pass++) {\n let mutated = false;\n for (const [key, field] of Object.entries(schema.fields)) {\n if (field.type !== \"derived\" || !field.formula) continue;\n const next = evaluateDerived(field.formula, { record: enriched, refs });\n if (next !== null && enriched[key] !== next) {\n enriched[key] = next;\n mutated = true;\n }\n }\n if (!mutated) break;\n }\n return enriched;\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_chunk = require("../chunk-CKQMccvm.cjs");
2
+ const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
3
3
  let node_path = require("node:path");
4
- node_path = require_chunk.__toESM(node_path, 1);
4
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  //#region src/file-change/index.ts
7
7
  var config = null;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/file-change/index.ts"],"sourcesContent":["// @mulmoclaude/core/file-change — shared \"this workspace file changed\"\n// broadcaster. A host write route calls `publishFileChange(relPath)` after a\n// successful write; subscribed UI tabs refetch. Extracted so MulmoClaude and\n// MulmoTerminal forward to the SAME plugin-scoped channels (the live-refresh\n// contract the markdown/html Views subscribe to) without duplicating the logic.\n//\n// Everything host-specific is INJECTED via `configureFileChangePublisher` (the\n// pubsub, the workspace root, the path→posix normaliser, the host's own primary\n// channel, the plugin-scope matchers, and any side-effect), so this package owns\n// only the orchestration + the `plugin:<scope>:file:<path>` channel format and\n// touches neither host's pubsub-channel config nor its frontend subscribers.\nimport { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/** Payload published on every file-change channel. `mtimeMs` is the post-write\n * modified time — monotonic, suitable for cache-busting (`?v=`) and out-of-order\n * drops. */\nexport interface FileChannelPayload {\n path: string;\n mtimeMs: number;\n}\n\n/** A plugin View that wants live-refresh: when `matches(posixPath)` is true, the\n * change is forwarded to `plugin:<scope>:file:<path>` (the channel the View's\n * runtime subscribes to). */\nexport interface FileChangeScope {\n scope: string;\n matches: (posixPath: string) => boolean;\n}\n\nexport interface FileChangePublisherConfig {\n /** Host pubsub publish. */\n publish: (channel: string, payload: FileChannelPayload) => void;\n /** Workspace root — joined with the relative path to stat the post-write mtime. */\n workspaceRoot: string;\n /** Normalise a workspace-relative path to POSIX (drives both `payload.path` and\n * every channel suffix, so they can't drift on mixed separators). */\n toPosix: (relativePath: string) => string;\n /** The host's primary file channel (e.g. a Files explorer's `file:<path>`).\n * Omit when the host has no general file subscriber (e.g. MulmoTerminal). */\n primaryChannel?: (posixPath: string) => string;\n /** Plugin Views to forward to. */\n pluginScopes?: FileChangeScope[];\n /** Optional side-effect after publishing (e.g. a topic-index regen). Receives the\n * posix path + payload; may itself call `publishFileChange` for derived files. */\n onPublished?: (posixPath: string, payload: FileChannelPayload) => void;\n /** Optional warn logger; a publish/stat failure logs but never throws (callers\n * fire-and-forget, so a throw would be an unhandled rejection). */\n warn?: (message: string, data?: Record<string, unknown>) => void;\n}\n\nlet config: FileChangePublisherConfig | null = null;\n\n/** Wire the publisher to a host. Call once at startup, before any write route. */\nexport function configureFileChangePublisher(cfg: FileChangePublisherConfig): void {\n config = cfg;\n}\n\n/** Clear the binding — test-only. */\nexport function resetFileChangePublisher(): void {\n config = null;\n}\n\n/** The plugin-scoped live-refresh channel a View subscribes to. */\nexport function pluginFileChannel(scope: string, posixPath: string): string {\n return `plugin:${scope}:file:${posixPath}`;\n}\n\n/** Publish a file-change for a workspace-relative path: the primary channel (if any)\n * + every matching plugin scope, with the post-write mtime. No-op until configured. */\nexport async function publishFileChange(relativePath: string): Promise<void> {\n const cfg = config;\n if (!cfg) return;\n // `relativePath` comes from the host's write routes. Contain it before doing\n // anything: a path that escapes the workspace is dropped entirely — we\n // neither stat an arbitrary file nor broadcast an out-of-workspace path to\n // subscribers or host side-effects. Defence-in-depth, since this is the\n // shared package both hosts use. `root` carries a trailing separator and the\n // guard is a single `startsWith` early-return — the canonical containment\n // shape (so the check is unambiguous for both readers and static analysis).\n const root = path.resolve(cfg.workspaceRoot) + path.sep;\n const absPath = path.join(root, relativePath);\n if (!absPath.startsWith(root)) {\n cfg.warn?.(\"ignoring file-change for path outside workspace\", { path: relativePath });\n return;\n }\n let mtimeMs: number;\n try {\n ({ mtimeMs } = await stat(absPath));\n } catch (err) {\n cfg.warn?.(\"stat failed; falling back to Date.now()\", { path: relativePath, error: errMsg(err) });\n mtimeMs = Date.now();\n }\n const posixPath = cfg.toPosix(relativePath);\n const payload: FileChannelPayload = { path: posixPath, mtimeMs };\n\n if (cfg.primaryChannel) {\n safePublish(cfg, cfg.primaryChannel(posixPath), payload, \"primary publish failed\");\n }\n for (const { scope, matches } of cfg.pluginScopes ?? []) {\n if (!matches(posixPath)) continue;\n safePublish(cfg, pluginFileChannel(scope, posixPath), payload, `${scope} plugin forward failed`);\n }\n cfg.onPublished?.(posixPath, payload);\n}\n\nfunction safePublish(cfg: FileChangePublisherConfig, channel: string, payload: FileChannelPayload, failMsg: string): void {\n try {\n cfg.publish(channel, payload);\n } catch (err) {\n cfg.warn?.(`${failMsg}; subscribers will miss this event`, { channel, error: errMsg(err) });\n }\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n"],"mappings":";;;;;;AAmDA,IAAI,SAA2C;;AAG/C,SAAgB,6BAA6B,KAAsC;CACjF,SAAS;AACX;;AAGA,SAAgB,2BAAiC;CAC/C,SAAS;AACX;;AAGA,SAAgB,kBAAkB,OAAe,WAA2B;CAC1E,OAAO,UAAU,MAAM,QAAQ;AACjC;;;AAIA,eAAsB,kBAAkB,cAAqC;CAC3E,MAAM,MAAM;CACZ,IAAI,CAAC,KAAK;CAQV,MAAM,OAAO,UAAA,QAAK,QAAQ,IAAI,aAAa,IAAI,UAAA,QAAK;CACpD,MAAM,UAAU,UAAA,QAAK,KAAK,MAAM,YAAY;CAC5C,IAAI,CAAC,QAAQ,WAAW,IAAI,GAAG;EAC7B,IAAI,OAAO,mDAAmD,EAAE,MAAM,aAAa,CAAC;EACpF;CACF;CACA,IAAI;CACJ,IAAI;EACF,CAAC,CAAE,WAAY,OAAA,GAAA,iBAAA,MAAW,OAAO;CACnC,SAAS,KAAK;EACZ,IAAI,OAAO,2CAA2C;GAAE,MAAM;GAAc,OAAO,OAAO,GAAG;EAAE,CAAC;EAChG,UAAU,KAAK,IAAI;CACrB;CACA,MAAM,YAAY,IAAI,QAAQ,YAAY;CAC1C,MAAM,UAA8B;EAAE,MAAM;EAAW;CAAQ;CAE/D,IAAI,IAAI,gBACN,YAAY,KAAK,IAAI,eAAe,SAAS,GAAG,SAAS,wBAAwB;CAEnF,KAAK,MAAM,EAAE,OAAO,aAAa,IAAI,gBAAgB,CAAC,GAAG;EACvD,IAAI,CAAC,QAAQ,SAAS,GAAG;EACzB,YAAY,KAAK,kBAAkB,OAAO,SAAS,GAAG,SAAS,GAAG,MAAM,uBAAuB;CACjG;CACA,IAAI,cAAc,WAAW,OAAO;AACtC;AAEA,SAAS,YAAY,KAAgC,SAAiB,SAA6B,SAAuB;CACxH,IAAI;EACF,IAAI,QAAQ,SAAS,OAAO;CAC9B,SAAS,KAAK;EACZ,IAAI,OAAO,GAAG,QAAQ,qCAAqC;GAAE;GAAS,OAAO,OAAO,GAAG;EAAE,CAAC;CAC5F;AACF;AAEA,SAAS,OAAO,KAAsB;CACpC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/file-change/index.ts"],"sourcesContent":["// @mulmoclaude/core/file-change — shared \"this workspace file changed\"\n// broadcaster. A host write route calls `publishFileChange(relPath)` after a\n// successful write; subscribed UI tabs refetch. Extracted so MulmoClaude and\n// MulmoTerminal forward to the SAME plugin-scoped channels (the live-refresh\n// contract the markdown/html Views subscribe to) without duplicating the logic.\n//\n// Everything host-specific is INJECTED via `configureFileChangePublisher` (the\n// pubsub, the workspace root, the path→posix normaliser, the host's own primary\n// channel, the plugin-scope matchers, and any side-effect), so this package owns\n// only the orchestration + the `plugin:<scope>:file:<path>` channel format and\n// touches neither host's pubsub-channel config nor its frontend subscribers.\nimport { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/** Payload published on every file-change channel. `mtimeMs` is the post-write\n * modified time — monotonic, suitable for cache-busting (`?v=`) and out-of-order\n * drops. */\nexport interface FileChannelPayload {\n path: string;\n mtimeMs: number;\n}\n\n/** A plugin View that wants live-refresh: when `matches(posixPath)` is true, the\n * change is forwarded to `plugin:<scope>:file:<path>` (the channel the View's\n * runtime subscribes to). */\nexport interface FileChangeScope {\n scope: string;\n matches: (posixPath: string) => boolean;\n}\n\nexport interface FileChangePublisherConfig {\n /** Host pubsub publish. */\n publish: (channel: string, payload: FileChannelPayload) => void;\n /** Workspace root — joined with the relative path to stat the post-write mtime. */\n workspaceRoot: string;\n /** Normalise a workspace-relative path to POSIX (drives both `payload.path` and\n * every channel suffix, so they can't drift on mixed separators). */\n toPosix: (relativePath: string) => string;\n /** The host's primary file channel (e.g. a Files explorer's `file:<path>`).\n * Omit when the host has no general file subscriber (e.g. MulmoTerminal). */\n primaryChannel?: (posixPath: string) => string;\n /** Plugin Views to forward to. */\n pluginScopes?: FileChangeScope[];\n /** Optional side-effect after publishing (e.g. a topic-index regen). Receives the\n * posix path + payload; may itself call `publishFileChange` for derived files. */\n onPublished?: (posixPath: string, payload: FileChannelPayload) => void;\n /** Optional warn logger; a publish/stat failure logs but never throws (callers\n * fire-and-forget, so a throw would be an unhandled rejection). */\n warn?: (message: string, data?: Record<string, unknown>) => void;\n}\n\nlet config: FileChangePublisherConfig | null = null;\n\n/** Wire the publisher to a host. Call once at startup, before any write route. */\nexport function configureFileChangePublisher(cfg: FileChangePublisherConfig): void {\n config = cfg;\n}\n\n/** Clear the binding — test-only. */\nexport function resetFileChangePublisher(): void {\n config = null;\n}\n\n/** The plugin-scoped live-refresh channel a View subscribes to. */\nexport function pluginFileChannel(scope: string, posixPath: string): string {\n return `plugin:${scope}:file:${posixPath}`;\n}\n\n/** Publish a file-change for a workspace-relative path: the primary channel (if any)\n * + every matching plugin scope, with the post-write mtime. No-op until configured. */\nexport async function publishFileChange(relativePath: string): Promise<void> {\n const cfg = config;\n if (!cfg) return;\n // `relativePath` comes from the host's write routes. Contain it before doing\n // anything: a path that escapes the workspace is dropped entirely — we\n // neither stat an arbitrary file nor broadcast an out-of-workspace path to\n // subscribers or host side-effects. Defence-in-depth, since this is the\n // shared package both hosts use. `root` carries a trailing separator and the\n // guard is a single `startsWith` early-return — the canonical containment\n // shape (so the check is unambiguous for both readers and static analysis).\n const root = path.resolve(cfg.workspaceRoot) + path.sep;\n const absPath = path.join(root, relativePath);\n if (!absPath.startsWith(root)) {\n cfg.warn?.(\"ignoring file-change for path outside workspace\", { path: relativePath });\n return;\n }\n let mtimeMs: number;\n try {\n ({ mtimeMs } = await stat(absPath));\n } catch (err) {\n cfg.warn?.(\"stat failed; falling back to Date.now()\", { path: relativePath, error: errMsg(err) });\n mtimeMs = Date.now();\n }\n const posixPath = cfg.toPosix(relativePath);\n const payload: FileChannelPayload = { path: posixPath, mtimeMs };\n\n if (cfg.primaryChannel) {\n safePublish(cfg, cfg.primaryChannel(posixPath), payload, \"primary publish failed\");\n }\n for (const { scope, matches } of cfg.pluginScopes ?? []) {\n if (!matches(posixPath)) continue;\n safePublish(cfg, pluginFileChannel(scope, posixPath), payload, `${scope} plugin forward failed`);\n }\n cfg.onPublished?.(posixPath, payload);\n}\n\nfunction safePublish(cfg: FileChangePublisherConfig, channel: string, payload: FileChannelPayload, failMsg: string): void {\n try {\n cfg.publish(channel, payload);\n } catch (err) {\n cfg.warn?.(`${failMsg}; subscribers will miss this event`, { channel, error: errMsg(err) });\n }\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n"],"mappings":";;;;;;AAmDA,IAAI,SAA2C;;AAG/C,SAAgB,6BAA6B,KAAsC;CACjF,SAAS;AACX;;AAGA,SAAgB,2BAAiC;CAC/C,SAAS;AACX;;AAGA,SAAgB,kBAAkB,OAAe,WAA2B;CAC1E,OAAO,UAAU,MAAM,QAAQ;AACjC;;;AAIA,eAAsB,kBAAkB,cAAqC;CAC3E,MAAM,MAAM;CACZ,IAAI,CAAC,KAAK;CAQV,MAAM,OAAO,UAAA,QAAK,QAAQ,IAAI,aAAa,IAAI,UAAA,QAAK;CACpD,MAAM,UAAU,UAAA,QAAK,KAAK,MAAM,YAAY;CAC5C,IAAI,CAAC,QAAQ,WAAW,IAAI,GAAG;EAC7B,IAAI,OAAO,mDAAmD,EAAE,MAAM,aAAa,CAAC;EACpF;CACF;CACA,IAAI;CACJ,IAAI;EACF,CAAC,CAAE,WAAY,OAAA,GAAA,iBAAA,KAAA,CAAW,OAAO;CACnC,SAAS,KAAK;EACZ,IAAI,OAAO,2CAA2C;GAAE,MAAM;GAAc,OAAO,OAAO,GAAG;EAAE,CAAC;EAChG,UAAU,KAAK,IAAI;CACrB;CACA,MAAM,YAAY,IAAI,QAAQ,YAAY;CAC1C,MAAM,UAA8B;EAAE,MAAM;EAAW;CAAQ;CAE/D,IAAI,IAAI,gBACN,YAAY,KAAK,IAAI,eAAe,SAAS,GAAG,SAAS,wBAAwB;CAEnF,KAAK,MAAM,EAAE,OAAO,aAAa,IAAI,gBAAgB,CAAC,GAAG;EACvD,IAAI,CAAC,QAAQ,SAAS,GAAG;EACzB,YAAY,KAAK,kBAAkB,OAAO,SAAS,GAAG,SAAS,GAAG,MAAM,uBAAuB;CACjG;CACA,IAAI,cAAc,WAAW,OAAO;AACtC;AAEA,SAAS,YAAY,KAAgC,SAAiB,SAA6B,SAAuB;CACxH,IAAI;EACF,IAAI,QAAQ,SAAS,OAAO;CAC9B,SAAS,KAAK;EACZ,IAAI,OAAO,GAAG,QAAQ,qCAAqC;GAAE;GAAS,OAAO,OAAO,GAAG;EAAE,CAAC;CAC5F;AACF;AAEA,SAAS,OAAO,KAAsB;CACpC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD"}
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_notifier = require("../notifier-lJ4v2Y6B.cjs");
2
+ const require_notifier = require("../notifier-bS8IEeLA.cjs");
3
3
  exports.HISTORY_CAP = require_notifier.HISTORY_CAP;
4
4
  exports.NOTIFIER_LIFECYCLES = require_notifier.NOTIFIER_LIFECYCLES;
5
5
  exports.NOTIFIER_LIMITS = require_notifier.NOTIFIER_LIMITS;
@@ -1,2 +1,2 @@
1
- import { _ as HISTORY_CAP, a as get, c as listFor, d as publish, f as resetNotifier, g as validatePublishInput, h as NOTIFIER_LIMITS, i as configureNotifier, l as listHistory, m as updateForPlugin, n as clear, o as getForPlugin, p as setNotifierFilePaths, r as clearForPlugin, s as listAll, t as cancel, u as onEvent, v as NOTIFIER_LIFECYCLES, y as NOTIFIER_SEVERITIES } from "../notifier-6PjsLxLm.js";
1
+ import { _ as HISTORY_CAP, a as get, c as listFor, d as publish, f as resetNotifier, g as validatePublishInput, h as NOTIFIER_LIMITS, i as configureNotifier, l as listHistory, m as updateForPlugin, n as clear, o as getForPlugin, p as setNotifierFilePaths, r as clearForPlugin, s as listAll, t as cancel, u as onEvent, v as NOTIFIER_LIFECYCLES, y as NOTIFIER_SEVERITIES } from "../notifier-ChpY0XrY.js";
2
2
  export { HISTORY_CAP, NOTIFIER_LIFECYCLES, NOTIFIER_LIMITS, NOTIFIER_SEVERITIES, cancel, clear, clearForPlugin, configureNotifier, get, getForPlugin, listAll, listFor, listHistory, onEvent, publish, resetNotifier, setNotifierFilePaths, updateForPlugin, validatePublishInput };
@@ -461,4 +461,4 @@ async function listHistory() {
461
461
  //#endregion
462
462
  export { HISTORY_CAP as _, get as a, listFor as c, publish as d, resetNotifier as f, validatePublishInput as g, NOTIFIER_LIMITS as h, configureNotifier as i, listHistory as l, updateForPlugin as m, clear as n, getForPlugin as o, setNotifierFilePaths as p, clearForPlugin as r, listAll as s, cancel as t, onEvent as u, NOTIFIER_LIFECYCLES as v, NOTIFIER_SEVERITIES as y };
463
463
 
464
- //# sourceMappingURL=notifier-6PjsLxLm.js.map
464
+ //# sourceMappingURL=notifier-ChpY0XrY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"notifier-6PjsLxLm.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 }\n | { type: \"cleared\"; id: string }\n | { type: \"cancelled\"; id: string }\n | { 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 { 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 interface NotifierLogger {\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\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\nexport async function clear(entryId: string): 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: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function cancel(entryId: string): 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: { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cancelled\"),\n };\n });\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
+ {"version":3,"file":"notifier-ChpY0XrY.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 }\n | { type: \"cleared\"; id: string }\n | { type: \"cancelled\"; id: string }\n | { 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 { 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 interface NotifierLogger {\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\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\nexport async function clear(entryId: string): 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: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function cancel(entryId: string): 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: { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cancelled\"),\n };\n });\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,4 +1,3 @@
1
- require("./chunk-CKQMccvm.cjs");
2
1
  let node_fs = require("node:fs");
3
2
  let node_crypto = require("node:crypto");
4
3
  //#region src/notifier/types.ts
@@ -575,4 +574,4 @@ Object.defineProperty(exports, "validatePublishInput", {
575
574
  }
576
575
  });
577
576
 
578
- //# sourceMappingURL=notifier-lJ4v2Y6B.cjs.map
577
+ //# sourceMappingURL=notifier-bS8IEeLA.cjs.map