@mulmoclaude/core 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/assets/helps/billing-clients-worklog.md +15 -12
  2. package/assets/helps/collection-skills.md +67 -22
  3. package/assets/helps/todo-collection.md +13 -7
  4. package/dist/{deriveAll-BQ_p5HSB.cjs → calendarGrid-CaR-q36Y.js} +352 -74
  5. package/dist/calendarGrid-CaR-q36Y.js.map +1 -0
  6. package/dist/calendarGrid-gN4sei7s.cjs +1070 -0
  7. package/dist/calendarGrid-gN4sei7s.cjs.map +1 -0
  8. package/dist/collection/core/backlinks.d.ts +20 -0
  9. package/dist/collection/core/ids.d.ts +12 -0
  10. package/dist/collection/core/recordZ.d.ts +23 -0
  11. package/dist/collection/core/schema.d.ts +72 -418
  12. package/dist/collection/core/schemaZ.d.ts +878 -0
  13. package/dist/collection/core/templatePath.d.ts +34 -0
  14. package/dist/collection/core/uiTypes.d.ts +22 -0
  15. package/dist/collection/core/where.d.ts +9 -15
  16. package/dist/collection/index.cjs +50 -330
  17. package/dist/collection/index.cjs.map +1 -1
  18. package/dist/collection/index.d.ts +2 -0
  19. package/dist/collection/index.js +14 -301
  20. package/dist/collection/index.js.map +1 -1
  21. package/dist/collection/paths.cjs +1 -1
  22. package/dist/collection/paths.cjs.map +1 -1
  23. package/dist/collection/paths.js +1 -1
  24. package/dist/collection/paths.js.map +1 -1
  25. package/dist/collection/registry/server/index.cjs +1 -1
  26. package/dist/collection/registry/server/index.js +1 -1
  27. package/dist/collection/server/discovery.d.ts +2 -244
  28. package/dist/collection/server/index.cjs +8 -2
  29. package/dist/collection/server/index.d.ts +1 -0
  30. package/dist/collection/server/index.js +3 -2
  31. package/dist/collection/server/io.d.ts +9 -0
  32. package/dist/collection/server/ontology.d.ts +35 -0
  33. package/dist/collection/server/templatePath.d.ts +1 -34
  34. package/dist/collection/server/validate.d.ts +11 -7
  35. package/dist/collection-watchers/index.cjs +5 -4
  36. package/dist/collection-watchers/index.cjs.map +1 -1
  37. package/dist/collection-watchers/index.js +3 -2
  38. package/dist/collection-watchers/index.js.map +1 -1
  39. package/dist/feeds/index.cjs +4 -4
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/ingestTypes.d.ts +15 -40
  42. package/dist/feeds/server/index.cjs +5 -5
  43. package/dist/feeds/server/index.js +3 -3
  44. package/dist/{ingestTypes-C7EheYZX.cjs → ingestTypes-DG8RExWt.cjs} +3 -3
  45. package/dist/ingestTypes-DG8RExWt.cjs.map +1 -0
  46. package/dist/{ingestTypes-CmJeOUJc.js → ingestTypes-V4upKeBx.js} +2 -2
  47. package/dist/ingestTypes-V4upKeBx.js.map +1 -0
  48. package/dist/{server-DKXXFTbw.js → server-BNTR7ySK.js} +531 -196
  49. package/dist/server-BNTR7ySK.js.map +1 -0
  50. package/dist/{server-G6GtOdaW.cjs → server-bsNrf2yW.cjs} +572 -213
  51. package/dist/server-bsNrf2yW.cjs.map +1 -0
  52. package/package.json +1 -1
  53. package/dist/deriveAll-BQ_p5HSB.cjs.map +0 -1
  54. package/dist/deriveAll-CMFXDQ_G.js +0 -506
  55. package/dist/deriveAll-CMFXDQ_G.js.map +0 -1
  56. package/dist/ingestTypes-C7EheYZX.cjs.map +0 -1
  57. package/dist/ingestTypes-CmJeOUJc.js.map +0 -1
  58. package/dist/server-DKXXFTbw.js.map +0 -1
  59. package/dist/server-G6GtOdaW.cjs.map +0 -1
@@ -1,11 +1,11 @@
1
- import { Where } from './where';
1
+ import { z } from 'zod';
2
+ import { ActionSpecZ, CollectionSchemaZ, CustomViewZ, DynamicIconRuleZ, DynamicIconSourceZ, DynamicIconSpecZ, EveryFieldDrivenZ, EveryLiteralZ, EveryZ, FieldSpecZ, IngestZ, SpawnZ, SubFieldSpecZ, WhenZ } from './schemaZ';
2
3
  /** Minimal "this collection is a feed" descriptor carried on the schema.
3
4
  * Deliberately narrow — the canonical collection contract stays
4
5
  * independent of the host's feeds subsystem. The host's richer retrieval
5
- * spec (`IngestSpec` in `server/workspace/feeds/ingestTypes.ts`)
6
- * `extends CollectionIngest`, so feed code reads the extra fields by
7
- * typing feed schemas with that subtype; collection rendering only needs
8
- * these three + the presence check. */
6
+ * spec (`IngestSpec` in `feeds/ingestTypes.ts`) is a subtype, so feed code
7
+ * reads the extra fields by typing feed schemas with that subtype;
8
+ * collection rendering only needs these three + the presence check. */
9
9
  export interface CollectionIngest {
10
10
  kind: string;
11
11
  schedule: string;
@@ -44,439 +44,93 @@ export type AgentIngestKind = typeof AGENT_INGEST_KIND;
44
44
  /** Refresh cadences a Feed's `ingest.schedule` may declare. */
45
45
  export declare const FEED_SCHEDULES: readonly ["hourly", "daily", "weekly", "on-demand"];
46
46
  export type FeedSchedule = (typeof FEED_SCHEDULES)[number];
47
- export type CollectionFieldType = "string" | "text" | "email" | "number" | "date" | "datetime" | "boolean" | "markdown" | "ref" | "money" | "enum" | "table" | "derived" | "embed" | "image" | "file" | "toggle";
48
47
  export type CollectionSource = "user" | "project" | "feed";
49
- /** Recurrence unit for a `spawn.every` advance. */
50
- export type CollectionRecurUnit = "day" | "week" | "month" | "year";
51
- /** How a `spawn` advances the source item's `triggerField` date to
52
- * produce the successor's. All arithmetic is done on the civil
53
- * (year, month, day) triple never by adding milliseconds — so month
54
- * lengths and leap years are handled correctly. */
55
- export interface CollectionEvery {
56
- unit: CollectionRecurUnit;
57
- /** Number of `unit`s to advance (≥ 1). `interval: 3` + `unit: "month"`
58
- * = quarterly; `interval: 1` + `unit: "year"` = annual. */
59
- interval: number;
60
- /** Day-of-month anchor for `month`/`year` units. The CANONICAL day
61
- * read from the rule, never re-derived from the prior concrete date,
62
- * so "31st of every month" yields 31 → 28/29 → 31 → 30 … with no
63
- * drift (it is clamped per-month at compute time, not stored
64
- * clamped). `"last"` always means the last day of the target month.
65
- * Omitted ⇒ preserve the source date's day (safe for days ≤ 28).
66
- * Ignored for `day`/`week` units. */
67
- dayOfMonth?: number | "last";
68
- }
69
- /** Field-driven recurrence: the advance interval is selected PER RECORD by
70
- * the value of an `enum` field (`fromField`), looked up in `map`. Lets one
71
- * collection mix daily / weekly / monthly obligations in a single list — the
72
- * host reads `record[fromField]`, finds the matching `CollectionEvery`, and
73
- * advances by it. `fromField` must point at a top-level `enum` field whose
74
- * `values` the `map` keys exactly cover (validated at discovery), and must
75
- * itself be carried/`set` onto the successor so the chain keeps recurring. */
76
- export interface CollectionEveryFieldDriven {
77
- /** Top-level `enum` field whose value selects the interval. */
78
- fromField: string;
79
- /** Interval per enum value. Keys exactly cover `fromField`'s `values`;
80
- * each value is a literal {@link CollectionEvery}. */
81
- map: Record<string, CollectionEvery>;
82
- }
83
- /** The `every` of a `spawn`: either a single literal interval applied to
84
- * every record, or a per-record interval selected by an `enum` field. The
85
- * literal arm is what `advanceTriggerDate` consumes — the field-driven arm
86
- * is resolved down to one of its `map` values before the date math runs. */
87
- export type CollectionSpawnEvery = CollectionEvery | CollectionEveryFieldDriven;
88
- /** Narrowing guard: true when `every` is the field-driven arm. */
89
- export declare function isFieldDrivenEvery(every: CollectionSpawnEvery): every is CollectionEveryFieldDriven;
90
- /** Host-driven recurrence: when a record satisfies `when`, the host
91
- * creates the next record with a forward-advanced `triggerField` date.
92
- * The successor's id and contents are a pure function of (source
93
- * record, this rule); creation is create-if-absent, so the mechanism
94
- * stays convergent — observing the predicate N times writes one
95
- * successor. Requires the schema to declare `triggerField`. */
96
- export interface CollectionSpawn {
97
- /** Predicate that fires the spawn (a `CollectionWhen`). Defaults to
98
- * "`completionField` value ∈ `completionDoneValues`" (i.e. spawn the
99
- * next instance when this one is done). */
100
- when?: CollectionWhen;
101
- /** How to advance `triggerField` from the source to the successor —
102
- * either a single literal interval or a per-record, field-driven map. */
103
- every: CollectionSpawnEvery;
104
- /** Record fields copied verbatim onto the successor. Fields not listed
105
- * here, not in `set`, and not the trigger / primary keys start
106
- * blank. */
107
- carry?: string[];
108
- /** Fields forced to fixed values on the successor (typically resetting
109
- * the status field to its pending value). */
110
- set?: Record<string, unknown>;
111
- }
112
- /** The kind of work an action kicks off. v1 ships only `"chat"` —
113
- * start a new chat in a role with a templated seed prompt. The enum
114
- * reserves room for a future `"mutate"` (status transitions) without
115
- * another schema-shape change. */
116
- export type CollectionActionKind = "chat";
48
+ /** One field of a record — a discriminated union on `type`; see the variant
49
+ * docs in `./schemaZ` (`FieldSpecZ`). */
50
+ export type CollectionFieldSpec = z.infer<typeof FieldSpecZ>;
51
+ /** A `table` field's row sub-schema entry the field union minus `table` /
52
+ * `derived` / display-only types (see `SubFieldSpecZ`). */
53
+ export type CollectionSubFieldSpec = z.infer<typeof SubFieldSpecZ>;
54
+ export type CollectionFieldType = CollectionFieldSpec["type"];
55
+ /** derived/embed/backlinks/toggle are host-computed or projected — never
56
+ * written to the record JSON, so required / value checks and edit-draft
57
+ * slots must not apply to them. THE single source for "computed"
58
+ * lives here (zod-free at runtime) so browser code (`./draft`) and the
59
+ * zod record compiler (`./recordZ`, which re-exports it) share one set
60
+ * instead of drifting copies. */
61
+ export declare const COMPUTED_TYPES: ReadonlySet<CollectionFieldType>;
117
62
  /** Optional visibility predicate: the target (an action button or a
118
63
  * field) renders only when the open record's `field` (stringified) is
119
64
  * one of `in`. Generic and domain-free — the host evaluates it against
120
65
  * the record with no knowledge of what the field means. Absent ⇒
121
66
  * always shown. */
122
- export interface CollectionWhen {
123
- /** Top-level record field key whose value gates visibility. */
124
- field: string;
125
- /** Allowed values; the target shows when `String(record[field])` is
126
- * one of these. Non-empty. */
127
- in: string[];
128
- }
67
+ export type CollectionWhen = z.infer<typeof WhenZ>;
129
68
  /** @deprecated Name retained for back-compat; use {@link CollectionWhen}.
130
- * Both actions and fields share the same predicate shape. */
69
+ * Both actions and fields share the same predicate shape. No in-repo
70
+ * consumers, but the package is public API (MulmoTerminal). */
131
71
  export type CollectionActionWhen = CollectionWhen;
72
+ /** A schema-declared, per-record action rendered as a button in the
73
+ * read-only detail view. Pure UI/behaviour directive — never stored,
74
+ * never validated against record data. All domain specifics (label,
75
+ * role, template) live in the schema / skill folder, so the host
76
+ * stays generic. See `ActionSpecZ`. */
77
+ export type CollectionAction = z.infer<typeof ActionSpecZ>;
78
+ /** The kind of work an action kicks off. v1 ships only `"chat"`; the enum
79
+ * (in `ActionSpecZ`) reserves room for a future `"mutate"`. */
80
+ export type CollectionActionKind = CollectionAction["kind"];
81
+ /** A custom (LLM-authored) HTML view for a collection. The host renders
82
+ * `file` in a sandboxed iframe over the collection's records; the view
83
+ * reaches its data only through a slug- and capability-scoped token (see
84
+ * `server/api/auth/viewToken.ts`). Pure data — the host holds no
85
+ * view-specific code; meaning lives in the HTML file + this registration.
86
+ * See `CustomViewZ` for the per-key contracts. */
87
+ export type CollectionCustomView = z.infer<typeof CustomViewZ>;
132
88
  /** What a custom view's capability token is allowed to do against the
133
89
  * collection's data endpoint. `read` returns enriched records (getItems
134
90
  * semantics); `write` validates-and-stores rows (putItems semantics).
135
91
  * There is deliberately no `delete` — a view can never do more than the
136
92
  * agent's own `manageCollection` tool. */
137
- export type CollectionViewCapability = "read" | "write";
138
- /** A custom (LLM-authored) HTML view for a collection. The host renders
139
- * `file` in a sandboxed iframe over the collection's records; the view
140
- * reaches its data only through a slug- and capability-scoped token (see
141
- * `server/api/auth/viewToken.ts`). Pure data the host holds no
142
- * view-specific code; meaning lives in the HTML file + this registration. */
143
- export interface CollectionCustomView {
144
- /** Stable id; the view-mode selector key (`custom:<id>`) and the
145
- * capability-token clamp key. Must be a valid slug. */
146
- id: string;
147
- /** Button label in the view-mode selector (author-authored, like field
148
- * labels not run through i18n). */
149
- label: string;
150
- /** Optional Material-icon name for the selector button. */
151
- icon?: string;
152
- /** Skill-relative path to the HTML file under `views/` (e.g.
153
- * `views/year.html`). Path-safe, must end in `.html`. */
154
- file: string;
155
- /** Optional skill-relative path to a JSON translation dictionary co-located
156
- * with the view (e.g. `views/year.i18n.json`). Shape mirrors **vue-i18n
157
- * locale messages** so an author can lift their app's locale JSON
158
- * verbatim:
159
- *
160
- * ```json
161
- * { "en": { "next": "Next", "hello": "Hello, {name}" },
162
- * "ja": { "next": "次へ", "hello": "{name} さん、こんにちは" } }
163
- * ```
164
- *
165
- * The host picks the block matching the active app locale (fallback
166
- * `"en"`, else `{}`) and injects ONLY that flat string map into
167
- * `window.__MC_VIEW.dict`. The iframe-side helper
168
- * `__MC_VIEW.t(key, named?)` mirrors vue-i18n's `t('msg', { name: 'x' })`
169
- * signature — named-interpolation only (no pluralization / linked
170
- * messages in v1; shipping vue-i18n's full runtime into every sandboxed
171
- * iframe would dominate page weight). The view never sees other locales'
172
- * strings. Constrained to `views/*.i18n.json` so authors keep the
173
- * translation file next to the HTML it translates. Absent ⇒ host-side
174
- * no-op (an i18n-less view keeps working; `t(key)` echoes the key). */
175
- i18n?: string;
176
- /** What the view may do with the data endpoint. Defaults to `["read"]`
177
- * (least privilege); declare `["read","write"]` only for views that
178
- * edit records. The mint endpoint clamps any requested caps to this. */
179
- capabilities?: CollectionViewCapability[];
180
- /** Where the view runs. Absent ⇒ `"desktop"` (this token/dataUrl contract).
181
- * `"mobile"` ⇒ a remote view for the phone client: served through the
182
- * command channel's `getRemoteView` over the postMessage contract
183
- * (`@mulmoclaude/core/remote-view` — no token, `connect-src 'none'`) and
184
- * previewed on desktop inside a phone-sized frame. */
185
- target?: "desktop" | "mobile";
186
- /** **Mobile-only** (ignored for desktop views, which use token-scoped
187
- * `capabilities`). The whitelist of field names a `target: "mobile"` view
188
- * may patch via `__MC_VIEW.updateItem(id, patch)`. Default-deny: absent or
189
- * empty ⇒ updates are refused host-side. Never include the primary key.
190
- * See plans/feat-remote-writable-view.md. */
191
- editableFields?: string[];
192
- /** **Mobile-only.** When `true`, a `target: "mobile"` view may remove a
193
- * record via `__MC_VIEW.deleteItem(id)`. Absent/`false` ⇒ deletes refused. */
194
- allowDelete?: boolean;
195
- /** **Mobile-only.** `image`-type fields whose workspace path the host inlines
196
- * as a downscaled `data:` URL thumbnail in `getItems` pages, so they render
197
- * on the phone (which can't reach the host's localhost). Opt-in (absent ⇒
198
- * none); the host projects `fields` first and only inlines the declared
199
- * fields that survive, within a per-page byte budget. Ignored for desktop
200
- * views (they resolve via `/api/files/raw`). See plans/feat-remote-view-images.md. */
201
- imageFields?: string[];
202
- /** **Mobile-only.** Longest-edge (px) an inlined `imageFields` thumbnail is
203
- * downscaled to. Absent ⇒ 512, clamped to `[64, 1024]`. */
204
- imageMaxEdge?: number;
205
- }
206
- /** A schema-declared, per-record action rendered as a button in the
207
- * read-only detail view. Pure UI/behaviour directive — never stored,
208
- * never validated against record data. All domain specifics (label,
209
- * role, template) live here in the schema / skill folder, so the host
210
- * stays generic. */
211
- export interface CollectionAction {
212
- /** Stable id (used in the dispatch route + testids). */
213
- id: string;
214
- /** Button text (English, like field labels). */
215
- label: string;
216
- /** Material-icon name shown on the button. */
217
- icon?: string;
218
- /** What the action does. v1: `"chat"`. */
219
- kind: CollectionActionKind;
220
- /** `kind: "chat"`: the role id the new chat runs in. */
221
- role: string;
222
- /** `kind: "chat"`: skill-relative path to the template file whose
223
- * text becomes the seed prompt body (e.g. `templates/invoice.md`). */
224
- template: string;
225
- /** Optional visibility predicate; the button renders only when the
226
- * open record matches (see CollectionWhen). Absent ⇒ always
227
- * shown. */
228
- when?: CollectionWhen;
229
- }
93
+ export type CollectionViewCapability = NonNullable<CollectionCustomView["capabilities"]>[number];
94
+ /** How a `spawn` advances the source item's `triggerField` date to
95
+ * produce the successor's. All arithmetic is done on the civil
96
+ * (year, month, day) triple never by adding milliseconds so month
97
+ * lengths and leap years are handled correctly. */
98
+ export type CollectionEvery = z.infer<typeof EveryLiteralZ>;
99
+ /** Recurrence unit for a `spawn.every` advance. */
100
+ export type CollectionRecurUnit = CollectionEvery["unit"];
101
+ /** Field-driven recurrence: the advance interval is selected PER RECORD by
102
+ * the value of an `enum` field (`fromField`), looked up in `map`. See
103
+ * `EveryFieldDrivenZ`. */
104
+ export type CollectionEveryFieldDriven = z.infer<typeof EveryFieldDrivenZ>;
105
+ /** The `every` of a `spawn`: either a single literal interval applied to
106
+ * every record, or a per-record interval selected by an `enum` field. The
107
+ * literal arm is what `advanceTriggerDate` consumes — the field-driven arm
108
+ * is resolved down to one of its `map` values before the date math runs. */
109
+ export type CollectionSpawnEvery = z.infer<typeof EveryZ>;
110
+ /** Narrowing guard: true when `every` is the field-driven arm. */
111
+ export declare function isFieldDrivenEvery(every: CollectionSpawnEvery): every is CollectionEveryFieldDriven;
112
+ /** Host-driven recurrence. See `SpawnZ`. */
113
+ export type CollectionSpawn = z.infer<typeof SpawnZ>;
230
114
  /** One rule in a `dynamicIcon.rules` list: when the resolved source
231
- * record matches `where` (an AND of typed conditions, see {@link Where}),
115
+ * record matches `where` (an AND of typed conditions, see `./where`),
232
116
  * the collection's effective launcher icon becomes `icon`. Evaluated top
233
117
  * to bottom — the first match wins. */
234
- export interface DynamicIconRule {
235
- where: Where;
236
- icon: string;
237
- }
118
+ export type DynamicIconRule = z.infer<typeof DynamicIconRuleZ>;
238
119
  /** Where a {@link DynamicIconSpec}'s source record comes from: a (possibly
239
120
  * cross-collection) pool of records, optionally narrowed by `where` and
240
121
  * reduced to a single record by `from`. */
241
- export interface DynamicIconSource {
242
- /** Slug of the collection to read records from — the collection itself
243
- * (self-reference) or another one (cross-collection). Required. */
244
- collection: string;
245
- /** How to reduce the (optionally `where`-filtered) pool to one record.
246
- * `"latest"` (default): the record with the greatest `orderBy` value.
247
- * `"first"` / `"when"`: the first record in the pool (storage order). */
248
- from?: "latest" | "first" | "when";
249
- /** Field compared for `from: "latest"`. Defaults to the source schema's
250
- * first `date`/`datetime` field (declaration order); when the source
251
- * has none, `"latest"` falls back to the last pool record. */
252
- orderBy?: string;
253
- /** Optional predicate (AND of typed conditions) narrowing the pool
254
- * before `from` reduces it to one record (e.g. restrict a shared
255
- * weather collection to one region). */
256
- where?: Where;
257
- }
122
+ export type DynamicIconSource = z.infer<typeof DynamicIconSourceZ>;
258
123
  /** Declarative "data state → icon" mapping for a collection's launcher
259
124
  * shortcut icon (see `CollectionSchema.dynamicIcon`). When absent, the
260
- * launcher icon is the static `schema.icon`, unchanged from before this
261
- * field existed. */
262
- export interface DynamicIconSpec {
263
- /** The record the `rules` are evaluated against. Required. */
264
- source: DynamicIconSource;
265
- /** First-match-wins list of "resolved record matches `where` → `icon`"
266
- * rules. */
267
- rules: DynamicIconRule[];
268
- /** Icon used when no source record resolves or no rule matches.
269
- * Defaults to the collection's own static `schema.icon`. */
270
- fallback?: string;
271
- }
272
- export interface CollectionFieldSpec {
273
- type: CollectionFieldType;
274
- label: string;
275
- /** True for the field whose value is the record's filename (no
276
- * separate auto-id). Exactly one field per schema may set this. */
277
- primary?: boolean;
278
- required?: boolean;
279
- /** When `type === "ref"` or `type === "embed"`: the slug of the
280
- * target collection. For `ref` the record stores the target
281
- * item's primary-key slug and the host renders a clickable link
282
- * + dropdown picker. For `embed` the host pulls a *fixed* record
283
- * (see `id`) from the target and renders its fields read-only in
284
- * the detail view. Required for both; ignored on every other
285
- * type. */
286
- to?: string;
287
- /** When `type === "embed"`: the primary-key value of the fixed
288
- * record to pull from the `to` collection (e.g. `me` for the
289
- * singleton mc-profile). Nothing is stored on this record — the
290
- * embed is a display-only directive resolved at render time, so
291
- * it never appears in the list table or the edit form. Supply
292
- * either this (a fixed target, same for every record) or
293
- * `idField` (a per-record target) — exactly one. Ignored on every
294
- * other type. */
295
- id?: string;
296
- /** When `type === "embed"`: the name of a sibling top-level field
297
- * whose value names the target record's primary key — letting the
298
- * embed point at a *different* record per row (e.g. an invoice's
299
- * `issuerId` ref selects which `profile` to embed as the
300
- * bill-from block). The renderer reads `record[idField]` at render
301
- * time; an absent/empty value resolves to "no record" (the same
302
- * fail-soft as a missing fixed `id`). Mutually exclusive with `id`
303
- * — an embed must declare exactly one. Ignored on every other type. */
304
- idField?: string;
305
- /** When `type === "money"` (or `type === "derived"` with
306
- * `display: "money"`): a literal ISO 4217 currency code passed to
307
- * `Intl.NumberFormat` for display — fixed for every record. The
308
- * stored value is always a plain decimal number; currency is
309
- * presentation only. Mutually substitutable with `currencyField`:
310
- * a money field must declare at least one of the two. */
311
- currency?: string;
312
- /** When `type === "money"` (or `type === "derived"` with
313
- * `display: "money"`): the name of a sibling record field whose
314
- * value holds the ISO 4217 code, letting currency vary per record
315
- * (e.g. an invoice's `currency` enum). The renderer reads
316
- * `record[currencyField]` and falls back to the literal `currency`
317
- * (then "USD") when the field is absent or empty. Resolved against
318
- * the top-level record even for money sub-fields inside a table. */
319
- currencyField?: string;
320
- /** When `type === "enum"`: the closed set of allowed string
321
- * values. The form renders a `<select>` populated from this
322
- * list; storage is a plain string. Required when type is
323
- * `enum`; ignored on every other type. */
324
- values?: readonly string[];
325
- /** When `type === "table"`: the sub-schema for each row (a flat
326
- * record of non-table / non-derived field specs). Required when
327
- * type is `table`. v0 disallows nested tables and derived
328
- * columns to keep the editor + evaluator simple. */
329
- of?: Record<string, CollectionFieldSpec>;
330
- /** When `type === "derived"`: a tiny expression evaluated against
331
- * the record. Supports `+ - * /`, parens, identifier refs to
332
- * top-level fields, `sum(tableField[].col)`, and
333
- * `sum(tableField[].col * tableField[].col)`. See
334
- * `src/utils/collections/derivedFormula.ts`. Required when type
335
- * is `derived`. */
336
- formula?: string;
337
- /** When `type === "derived"`: an inner field type the computed
338
- * value should be rendered as (e.g. `"money"` so $1,234.56 is
339
- * formatted). Defaults to `"number"`. */
340
- display?: CollectionFieldType;
341
- /** Optional visibility predicate: this field renders only when the
342
- * record matches (e.g. hide a `rating` field until `visited` is
343
- * `true` via `{ field: "visited", in: ["true"] }`). Applies to the
344
- * list cell (blank when hidden), the edit form (hidden live as the
345
- * gating field changes), and the detail view. Purely presentational
346
- * — a hidden field's stored value is never cleared. `when.field`
347
- * must name another top-level field. Absent ⇒ always shown. Only
348
- * honoured on top-level fields, not inside a `table`'s `of`. */
349
- when?: CollectionWhen;
350
- /** When `type === "toggle"`: the name of the top-level `enum` field this
351
- * checkbox projects. The toggle stores nothing itself — it reads and
352
- * writes this field. Required when type is `toggle`; ignored otherwise.
353
- * Must name a real `enum` field. */
354
- field?: string;
355
- /** When `type === "toggle"`: the enum value that means "checked". The
356
- * box is checked when the projected `field` equals this; checking writes
357
- * it. Required when type is `toggle`; must be one of the enum's `values`. */
358
- onValue?: string;
359
- /** When `type === "toggle"`: the enum value written when the box is
360
- * unchecked. Required when type is `toggle`; must be one of the enum's
361
- * `values`. */
362
- offValue?: string;
363
- }
364
- export interface CollectionSchema {
365
- /** Human-facing collection name (sidebar, header). */
366
- title: string;
367
- /** Material-icon name shown next to the title. */
368
- icon: string;
369
- /** Workspace-relative folder holding one-JSON-per-record. Validated
370
- * to live under the workspace root at load time. */
371
- dataPath: string;
372
- /** Field name whose value doubles as the record's filename. */
373
- primaryKey: string;
374
- /** When set, the collection is a singleton: at most one record,
375
- * whose primary key is fixed to this value (e.g. `me` for the
376
- * business profile). The host pre-fills + locks the create form's
377
- * primary key and hides Add once the record exists. */
378
- singleton?: string;
379
- /** Ordered map: insertion order = column order in the table view. */
380
- fields: Record<string, CollectionFieldSpec>;
381
- /** Optional per-record actions rendered as buttons in the detail
382
- * view (e.g. "Generate PDF"). Order = button order. */
383
- actions?: CollectionAction[];
384
- /** Optional collection-level actions rendered as buttons in the
385
- * collection header (e.g. "Extend the course"). Unlike `actions`,
386
- * these carry no record context: the seed prompt injects a compact
387
- * progress summary of every record instead. The `when` predicate is
388
- * not evaluated (there is no record to gate on). Order = button order. */
389
- collectionActions?: CollectionAction[];
390
- /** Name of the field whose value marks an item as "done". When set,
391
- * a notification fires on item create (unless the item is born done)
392
- * and clears when the field's value transitions into
393
- * `completionDoneValues`. Must name a real field in `fields`. */
394
- completionField?: string;
395
- /** The set of values for `completionField` that count as "done"
396
- * (e.g. `["Done"]` for a todo status field, `["paid"]` for an
397
- * invoice). Non-empty. Compared as strings. */
398
- completionDoneValues?: readonly string[];
399
- /** Name of the field whose value is shown as the human-readable
400
- * label in a completion notification's title (e.g. a `name` field,
401
- * so the bell reads `Contacts: Jane Doe` instead of the opaque
402
- * primaryKey). Must name a real field in `fields`. When unset — or
403
- * when the record's value for it is empty — the title falls back to
404
- * the record's primaryKey value. Display-only; never stored. */
405
- displayField?: string;
406
- /** Name of a `date` field that gates this item's completion
407
- * notification: the bell is suppressed until the clock reaches that
408
- * date (compared at day-granularity in the server's local timezone),
409
- * instead of firing on create. Requires `completionField` /
410
- * `completionDoneValues` (the bell still clears via the done value).
411
- * Must name a real `date` field. Absent ⇒ fire on create, as before. */
412
- triggerField?: string;
413
- /** Lead time in whole days: fire the bell this many days BEFORE
414
- * `triggerField` (so `10` shows the reminder 10 days early). The lead
415
- * is applied at fire time, not stored, so it composes with `spawn` —
416
- * every recurred cycle fires the same number of days before its own
417
- * trigger. Non-negative integer; requires `triggerField`. Default 0
418
- * (fire on the trigger date). */
419
- triggerLeadDays?: number;
420
- /** Host-driven recurrence. When set, requires `triggerField`. See
421
- * {@link CollectionSpawn}. */
422
- spawn?: CollectionSpawn;
423
- /** Name of a `date` field that anchors the optional calendar view: a
424
- * month grid where each record lands on the day cell matching this
425
- * field's value. When unset, the calendar toggle still appears if the
426
- * schema has any `date` field (the first one, in declaration order, is
427
- * used by default and is switchable in-view). Set this to pin a specific
428
- * anchor. Must name a real `date` field. */
429
- calendarField?: string;
430
- /** Name of a second `date` field marking the END of a multi-day span on
431
- * the calendar: the record renders from `calendarField` through this
432
- * date inclusive. Requires `calendarField`. Must name a real `date`
433
- * field. Absent ⇒ single-day placement. */
434
- calendarEndField?: string;
435
- /** Name of a string field holding a free-form time or time-range
436
- * (e.g. "14:00-17:00", "17:00-", "16:30") that places records on the
437
- * calendar's day (time-allocation) view. Consulted only when the calendar
438
- * date fields are date-only. Requires `calendarField`. */
439
- calendarTimeField?: string;
440
- /** Name of an `enum` field that groups records into columns on the
441
- * optional Kanban board: each record lands in the column matching its
442
- * value, with empty/unknown values collected in an "Uncategorized"
443
- * column. When unset, the Kanban toggle still appears if the schema has
444
- * any `enum` field (the first one, in declaration order, is used by
445
- * default and is switchable in-view). Set this to pin a specific group
446
- * field. Must name a real `enum` field. */
447
- kanbanField?: string;
448
- /** Optional custom (LLM-authored) HTML views, each rendered in a
449
- * sandboxed iframe over the records. Absent ⇒ only the built-in
450
- * field-derived views (table / calendar / kanban / dashboard). See
451
- * {@link CollectionCustomView}. */
452
- views?: CollectionCustomView[];
453
- /** Optional predicate that gates the completion bell: when set, the bell
454
- * fires only for records whose `String(record[notifyWhen.field])` is one
455
- * of `notifyWhen.in` (e.g. notify only `high`/`urgent` priority todos).
456
- * Reuses the `when` predicate shape. Requires `completionField` — it
457
- * narrows that bell rather than introducing a second one. The bell still
458
- * clears on done / delete / when the predicate stops matching. Absent ⇒
459
- * notify for every open record (the prior behaviour). `notifyWhen.field`
460
- * must name a real top-level field. */
461
- notifyWhen?: CollectionWhen;
462
- /** Optional scheduled-retrieval config. When present, the host refreshes
463
- * this collection on `ingest.schedule`. Two flavours: a declarative Feed
464
- * (`kind: rss/atom/http-json`) periodically fetches `ingest.url`, maps the
465
- * response into records, and upserts them by `primaryKey` — only feeds
466
- * discovered from the `<workspace>/feeds/` registry carry this. Or
467
- * `kind: "agent"`, valid on any (incl. skill-backed) collection: the host
468
- * dispatches a hidden background worker in `ingest.role` seeded with
469
- * `ingest.template`, and the worker edits the records itself. The host's
470
- * feeds subsystem narrows this to its richer `IngestSpec`. */
471
- ingest?: CollectionIngest;
472
- /** Optional data-driven override for the launcher shortcut icon: when
473
- * set, the host resolves a record from `dynamicIcon.source` and maps
474
- * it through `dynamicIcon.rules` to compute `CollectionSummary.icon`
475
- * instead of using this static `icon`. Absent ⇒ unchanged static-icon
476
- * behaviour. See `computeCollectionIcon` (server) and
477
- * `selectDynamicRecord`/`resolveIcon` (pure resolver). */
478
- dynamicIcon?: DynamicIconSpec;
479
- }
125
+ * launcher icon is the static `schema.icon`. */
126
+ export type DynamicIconSpec = z.infer<typeof DynamicIconSpecZ>;
127
+ /** The `ingest` block as the schema validator accepts it — a discriminated
128
+ * union on `kind` (declarative retrievers | agent worker). The feeds
129
+ * subsystem's `IngestSpec` is the same union under its historical name. */
130
+ export type CollectionIngestSpec = z.infer<typeof IngestZ>;
131
+ /** The whole `schema.json` contract. Key-level docs live on
132
+ * `CollectionSchemaZ` in `./schemaZ`. */
133
+ export type CollectionSchema = z.infer<typeof CollectionSchemaZ>;
480
134
  export interface CollectionSummary {
481
135
  slug: string;
482
136
  title: string;
@@ -496,5 +150,5 @@ export type CollectionItem = Record<string, unknown>;
496
150
  * of the sibling `idField` on this record (empty string when neither applies
497
151
  * — the caller renders that as "no record"). Pure + isomorphic so the server
498
152
  * projection (`derive.ts`) and the client preview (`useCollectionRendering`)
499
- * resolve embeds identically. */
153
+ * resolve embeds identically. Non-`embed` fields resolve to "no record". */
500
154
  export declare function embedTargetId(field: CollectionFieldSpec, record: CollectionItem | null): string;