@mulmoclaude/core 0.8.0 → 0.8.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 (37) hide show
  1. package/assets/helps/billing-clients-worklog.md +73 -5
  2. package/dist/collection/core/dynamicIcon.d.ts +24 -0
  3. package/dist/collection/core/schema.d.ts +55 -0
  4. package/dist/collection/core/where.d.ts +32 -0
  5. package/dist/collection/index.cjs +5 -1
  6. package/dist/collection/index.d.ts +2 -0
  7. package/dist/collection/index.js +2 -2
  8. package/dist/collection/registry/server/index.cjs +1 -1
  9. package/dist/collection/registry/server/index.js +1 -1
  10. package/dist/collection/server/discovery.d.ts +51 -0
  11. package/dist/collection/server/dynamicIcon.d.ts +10 -0
  12. package/dist/collection/server/index.cjs +2 -1
  13. package/dist/collection/server/index.d.ts +1 -0
  14. package/dist/collection/server/index.js +2 -2
  15. package/dist/collection-watchers/index.cjs +1 -1
  16. package/dist/collection-watchers/index.js +1 -1
  17. package/dist/{deriveAll-Cb9rWjan.cjs → deriveAll-BQ_p5HSB.cjs} +149 -1
  18. package/dist/deriveAll-BQ_p5HSB.cjs.map +1 -0
  19. package/dist/{deriveAll-D3wFH4Tw.js → deriveAll-CMFXDQ_G.js} +126 -2
  20. package/dist/deriveAll-CMFXDQ_G.js.map +1 -0
  21. package/dist/feeds/index.cjs +2 -2
  22. package/dist/feeds/index.js +2 -2
  23. package/dist/feeds/server/index.cjs +3 -3
  24. package/dist/feeds/server/index.js +3 -3
  25. package/dist/{ingestTypes-DhJ63Ogd.cjs → ingestTypes-C7EheYZX.cjs} +2 -2
  26. package/dist/{ingestTypes-DhJ63Ogd.cjs.map → ingestTypes-C7EheYZX.cjs.map} +1 -1
  27. package/dist/{ingestTypes-BtMZogMX.js → ingestTypes-CmJeOUJc.js} +2 -2
  28. package/dist/{ingestTypes-BtMZogMX.js.map → ingestTypes-CmJeOUJc.js.map} +1 -1
  29. package/dist/{server-BDxrLT41.js → server-DKXXFTbw.js} +96 -4
  30. package/dist/server-DKXXFTbw.js.map +1 -0
  31. package/dist/{server-U2d7Fb1h.cjs → server-G6GtOdaW.cjs} +101 -3
  32. package/dist/server-G6GtOdaW.cjs.map +1 -0
  33. package/package.json +4 -4
  34. package/dist/deriveAll-Cb9rWjan.cjs.map +0 -1
  35. package/dist/deriveAll-D3wFH4Tw.js.map +0 -1
  36. package/dist/server-BDxrLT41.js.map +0 -1
  37. package/dist/server-U2d7Fb1h.cjs.map +0 -1
@@ -26,8 +26,10 @@ user opens it at `/collections/<slug>`). **Do not use the `mc-` prefix.**
26
26
  > mimic other collections in the workspace. The whole point is a reproducible
27
27
  > billing suite. (If the user wants a *custom* collection instead, that's a
28
28
  > different task — use `config/helps/collection-skills.md`.) Existing records
29
- > under `data/clients/items` / `data/worklog/items` already match these schemas
30
- > and will render as-is once you author the skill — no data edits needed.
29
+ > under `data/clients/items` / `data/worklog/items` will render as-is **only
30
+ > when `worklog.clientId` values are already slugs AND a matching client
31
+ > record exists for each of them**. If either invariant is violated, the
32
+ > `ref` link breaks — the reconcile step below (§3) fixes both.
31
33
 
32
34
  ## Slug contract (do not change these)
33
35
 
@@ -206,10 +208,76 @@ If the user gives a clear "log N hours for X today", just write it. Use
206
208
 
207
209
  ---
208
210
 
211
+ ## 3. Reconcile pre-existing worklog data (MANDATORY before "Done")
212
+
213
+ Both skills are now authored. Before telling the user setup is complete,
214
+ audit any records that were already on disk under `data/worklog/items/` —
215
+ they may have been written before the `clients` collection existed, so
216
+ `clientId` values can be either display names (`"Singularity Society"`) or
217
+ missing-slug references. Both break the `ref` link at render time.
218
+
219
+ Run this reconcile every time — even when the setup looks fine — because
220
+ "looks fine" from the LLM side (schemas written, files present) is
221
+ distinct from "the ref actually resolves" (clientId is a valid slug AND
222
+ `data/clients/items/<slug>.json` exists).
223
+
224
+ **Steps** (idempotent; safe to re-run):
225
+
226
+ 1. **Inventory the existing worklog.** List `data/worklog/items/`. Read
227
+ every record and collect the distinct `clientId` values seen.
228
+ *Skip this step only when the directory does not exist at all.*
229
+ 2. **For each distinct `clientId` value**, classify it:
230
+ - **valid slug + matching client file exists** → no action.
231
+ - **valid slug but no matching `data/clients/items/<slug>.json`** →
232
+ create a stub client record: `id: <slug>`, `name` derived by
233
+ title-casing the slug (`acme-corp` → "Acme Corp"), other fields blank
234
+ so the user can fill them in later.
235
+ - **not a valid slug** (contains uppercase, spaces, `&`, `.`, etc.) →
236
+ (a) generate the slug (lowercase, spaces → `-`, drop punctuation,
237
+ collapse repeat hyphens, strip leading/trailing hyphens); (b)
238
+ **validate the generated slug against the `clients.id` contract**
239
+ (see § 1 record shape: `[a-z0-9-]`, 1–48 chars): if it is empty
240
+ (punctuation-only or all-whitespace input like `"!!!"`), or longer
241
+ than 48 chars, **stop and ask the user** for a valid slug rather
242
+ than writing an invalid filename. If it is >48 chars but has
243
+ meaningful prefix content, propose truncation at 48 (dropping any
244
+ trailing hyphen) and confirm with the user before proceeding; (c)
245
+ **check for slug collisions BEFORE writing anything**: if
246
+ `data/clients/items/<slug>.json` already exists AND its `name`
247
+ field does not match the display value being reconciled, stop and
248
+ disambiguate — either append a numeric suffix (`acme-corp-2`)
249
+ after confirming the new client is genuinely distinct, or ask the
250
+ user which client the worklog entries belong to. Do **not**
251
+ silently overwrite an existing client. Also stop when two
252
+ different display values in the current inventory slugify to the
253
+ same slug (e.g. `"ACME Corp"` and `"Acme Corp."`); ask the user
254
+ whether they're the same client (merge to one slug) or distinct
255
+ (assign each a suffixed slug); (d) create
256
+ `data/clients/items/<slug>.json` with `id: <slug>`, `name:
257
+ <original display value>`, other fields blank; (e) rewrite the raw
258
+ display value inside every affected `data/worklog/items/*.json`
259
+ to the new slug in the `clientId` field (preserve every other
260
+ field untouched).
261
+ 3. **Report** what changed: how many client records were created and how
262
+ many worklog `clientId` values were rewritten. Do not summarise the
263
+ raw records — point at `/collections/clients` and `/collections/worklog`.
264
+
265
+ If the worklog directory did not exist yet (fresh setup, no pre-existing
266
+ data), this step is a no-op and you can proceed straight to "Done".
267
+
268
+ **Don't skip this step because the LLM output says "existing records will
269
+ render as-is."** That claim only holds when both invariants (slug format
270
+ + matching client file) are already true; the reconcile confirms them.
271
+
272
+ ---
273
+
209
274
  ## Done
210
275
 
211
276
  Tell the user the two collections are ready at `/collections/clients` and
212
277
  `/collections/worklog`. The bridge mirrors the files and re-scans, so they appear
213
- without a restart. If invoicing is the goal, point them at the next step: run
214
- *"Set up invoicing for my business"* (the `config/helps/billing-invoice.md`
215
- recipe, which references these `clients` and pulls hours from this `worklog`).
278
+ without a restart. If the reconcile in §3 created client stubs or rewrote
279
+ `clientId` values, mention the counts so the user knows to fill in the
280
+ stubs' contact info. If invoicing is the goal, point them at the next
281
+ step: run *"Set up invoicing for my business"* (the
282
+ `config/helps/billing-invoice.md` recipe, which references these
283
+ `clients` and pulls hours from this `worklog`).
@@ -0,0 +1,24 @@
1
+ import { CollectionItem, CollectionSchema, DynamicIconSource, DynamicIconSpec } from './schema';
2
+ /** Reduce `records` to the one record that decides the effective icon, per
3
+ * `source`'s `where` filter + `from` strategy:
4
+ * - pool = `source.where`-filtered records, or every record when unset;
5
+ * - an empty pool resolves to `null` (no source record → fallback);
6
+ * - `from: "first"` / `"when"` → the first pool record (storage order);
7
+ * - `from: "latest"` (default), with `orderBy` given → the pool record
8
+ * whose `String(record[orderBy])` sorts highest;
9
+ * - `from: "latest"`, with no `orderBy` → the last pool record.
10
+ * `recordsById` (the source collection's records keyed by primaryKey)
11
+ * resolves any `valueFrom` reference inside `source.where`; omitted for
12
+ * callers with no cross-record lookups. */
13
+ export declare function selectDynamicRecord(records: CollectionItem[], source: DynamicIconSource, orderBy: string | undefined, recordsById?: Record<string, CollectionItem>): CollectionItem | null;
14
+ /** Map a resolved source record to the effective icon: `spec.fallback`
15
+ * (or the collection's own static `icon`) when there's no record or no
16
+ * rule matches; otherwise the `icon` of the first rule whose `where`
17
+ * matches the record. `recordsById` resolves any `valueFrom` reference
18
+ * inside a rule's `where`, same as `selectDynamicRecord`. */
19
+ export declare function resolveIcon(record: CollectionItem | null, spec: DynamicIconSpec, staticIcon: string, recordsById?: Record<string, CollectionItem>): string;
20
+ /** The first field key (declaration order) whose type is `date` or
21
+ * `datetime` — the default `orderBy` for `from: "latest"` when a
22
+ * `DynamicIconSource` doesn't name one. `undefined` when the schema has
23
+ * no date-like field. */
24
+ export declare function firstDateField(schema: CollectionSchema): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { Where } from './where';
1
2
  /** Minimal "this collection is a feed" descriptor carried on the schema.
2
3
  * Deliberately narrow — the canonical collection contract stays
3
4
  * independent of the host's feeds subsystem. The host's richer retrieval
@@ -226,6 +227,48 @@ export interface CollectionAction {
226
227
  * shown. */
227
228
  when?: CollectionWhen;
228
229
  }
230
+ /** One rule in a `dynamicIcon.rules` list: when the resolved source
231
+ * record matches `where` (an AND of typed conditions, see {@link Where}),
232
+ * the collection's effective launcher icon becomes `icon`. Evaluated top
233
+ * to bottom — the first match wins. */
234
+ export interface DynamicIconRule {
235
+ where: Where;
236
+ icon: string;
237
+ }
238
+ /** Where a {@link DynamicIconSpec}'s source record comes from: a (possibly
239
+ * cross-collection) pool of records, optionally narrowed by `where` and
240
+ * 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
+ }
258
+ /** Declarative "data state → icon" mapping for a collection's launcher
259
+ * 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
+ }
229
272
  export interface CollectionFieldSpec {
230
273
  type: CollectionFieldType;
231
274
  label: string;
@@ -426,12 +469,24 @@ export interface CollectionSchema {
426
469
  * `ingest.template`, and the worker edits the records itself. The host's
427
470
  * feeds subsystem narrows this to its richer `IngestSpec`. */
428
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;
429
479
  }
430
480
  export interface CollectionSummary {
431
481
  slug: string;
432
482
  title: string;
433
483
  icon: string;
434
484
  source: CollectionSource;
485
+ /** Slugs of the source collection(s) a `dynamicIcon` icon was computed
486
+ * from — present only when `schema.dynamicIcon` is set. Lets a client
487
+ * know which collection change-channel(s) to watch for a live icon
488
+ * update (see `useDynamicShortcutIcons`). */
489
+ iconSources?: string[];
435
490
  }
436
491
  export interface CollectionDetail extends CollectionSummary {
437
492
  schema: CollectionSchema;
@@ -0,0 +1,32 @@
1
+ /** Comparison operators one `WhereCond` may apply to `record[field]`. */
2
+ export type WhereOp = "eq" | "ne" | "in" | "gt" | "gte" | "lt" | "lte" | "contains";
3
+ /** Reads the comparison value from a field instead of a schema literal:
4
+ * - `record` set → another record: `recordsById[record][field]` (e.g. a
5
+ * `_config` singleton's `defaultCity`, following a per-user setting);
6
+ * - `record` omitted → the SAME record being matched (field-to-field, e.g.
7
+ * `spent > budget`). */
8
+ export interface ValueRef {
9
+ record?: string;
10
+ field: string;
11
+ }
12
+ /** One typed condition: `record[field] <op> value`. The comparison value is
13
+ * either a literal `value` (a plain string for every op except `in`, which
14
+ * takes the allowed set) or a `valueFrom` reference resolved against the
15
+ * `recordsById` map passed to `matchesWhere`. Exactly one of the two is
16
+ * expected — enforced by zod at the schema boundary (`server/discovery.ts`),
17
+ * not here. */
18
+ export interface WhereCond {
19
+ field: string;
20
+ op: WhereOp;
21
+ value?: string | string[];
22
+ valueFrom?: ValueRef;
23
+ }
24
+ /** A `where` clause is the AND of its conditions — every one must match. */
25
+ export type Where = WhereCond[];
26
+ /** True when `record` satisfies every condition in `where` (AND). An empty
27
+ * `where` matches everything. `recordsById` — the source collection's
28
+ * records keyed by primaryKey — resolves any `valueFrom` reference;
29
+ * omitted (default `{}`) for callers with no cross-record lookups, in
30
+ * which case every `valueFrom` condition is UNRESOLVED and so never
31
+ * matches. */
32
+ export declare function matchesWhere(where: Where, record: Record<string, unknown>, recordsById?: Record<string, Record<string, unknown>>): boolean;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_deriveAll = require("../deriveAll-Cb9rWjan.cjs");
2
+ const require_deriveAll = require("../deriveAll-BQ_p5HSB.cjs");
3
3
  //#region src/collection/core/presentCollection.ts
4
4
  var TOOL_NAME = "presentCollection";
5
5
  var TOOL_DEFINITION = {
@@ -769,12 +769,14 @@ exports.errorMessage = errorMessage;
769
769
  exports.evaluateDerived = require_deriveAll.evaluateDerived;
770
770
  exports.executePresentCollection = executePresentCollection;
771
771
  exports.fieldVisible = fieldVisible;
772
+ exports.firstDateField = require_deriveAll.firstDateField;
772
773
  exports.firstMissingRequiredField = firstMissingRequiredField;
773
774
  exports.isFieldDrivenEvery = require_deriveAll.isFieldDrivenEvery;
774
775
  exports.isSortableField = isSortableField;
775
776
  exports.itemIdOf = itemIdOf;
776
777
  exports.itemLabelOf = itemLabelOf;
777
778
  exports.labelFieldFor = labelFieldFor;
779
+ exports.matchesWhere = require_deriveAll.matchesWhere;
778
780
  exports.monthAnchorDate = monthAnchorDate;
779
781
  exports.nextSortDirection = nextSortDirection;
780
782
  exports.numericSortValue = numericSortValue;
@@ -783,8 +785,10 @@ exports.parseIsoDateTime = parseIsoDateTime;
783
785
  exports.parseTimeRange = parseTimeRange;
784
786
  exports.recordSpan = recordSpan;
785
787
  exports.resolveEnumColor = resolveEnumColor;
788
+ exports.resolveIcon = require_deriveAll.resolveIcon;
786
789
  exports.resolveRowRefs = require_deriveAll.resolveRowRefs;
787
790
  exports.rowFromItem = rowFromItem;
791
+ exports.selectDynamicRecord = require_deriveAll.selectDynamicRecord;
788
792
  exports.shortHexId = shortHexId;
789
793
  exports.sortItems = sortItems;
790
794
  exports.spanCoversDay = spanCoversDay;
@@ -4,6 +4,8 @@ export * from './core/presentCollection';
4
4
  export * from './core/enumColors';
5
5
  export * from './core/draft';
6
6
  export * from './core/actionVisible';
7
+ export * from './core/where';
8
+ export * from './core/dynamicIcon';
7
9
  export * from './core/derivedFormula';
8
10
  export * from './core/deriveAll';
9
11
  export * from './core/sortItems';
@@ -1,4 +1,4 @@
1
- import { a as FEED_SCHEDULES, c as isFieldDrivenEvery, i as AGENT_INGEST_KIND, n as resolveRowRefs, o as INGEST_KINDS, r as evaluateDerived, s as embedTargetId, t as deriveAll } from "../deriveAll-D3wFH4Tw.js";
1
+ import { a as resolveIcon, c as AGENT_INGEST_KIND, d as embedTargetId, f as isFieldDrivenEvery, i as firstDateField, l as FEED_SCHEDULES, n as resolveRowRefs, o as selectDynamicRecord, r as evaluateDerived, s as matchesWhere, t as deriveAll, u as INGEST_KINDS } from "../deriveAll-CMFXDQ_G.js";
2
2
  //#region src/collection/core/presentCollection.ts
3
3
  var TOOL_NAME = "presentCollection";
4
4
  var TOOL_DEFINITION = {
@@ -735,6 +735,6 @@ function defangForPrompt(value) {
735
735
  return value.replace(/[<>]/g, "").replace(/`/g, "'").replace(/\$\{/g, "$ {").replace(/\s+/g, " ").slice(0, DEFANG_MAX_LEN);
736
736
  }
737
737
  //#endregion
738
- export { AGENT_INGEST_KIND, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, TOOL_DEFINITION, TOOL_NAME, actionVisible, assignLanes, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, compareSortValues, compareYmd, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldVisible, firstMissingRequiredField, isFieldDrivenEvery, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, monthAnchorDate, nextSortDirection, numericSortValue, parseIsoDate, parseIsoDateTime, parseTimeRange, recordSpan, resolveEnumColor, resolveRowRefs, rowFromItem, shortHexId, sortItems, spanCoversDay, stringSortValue, whenMatches, ymdKey };
738
+ export { AGENT_INGEST_KIND, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, TOOL_DEFINITION, TOOL_NAME, actionVisible, assignLanes, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, compareSortValues, compareYmd, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldVisible, firstDateField, firstMissingRequiredField, isFieldDrivenEvery, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, matchesWhere, monthAnchorDate, nextSortDirection, numericSortValue, parseIsoDate, parseIsoDateTime, parseTimeRange, recordSpan, resolveEnumColor, resolveIcon, resolveRowRefs, rowFromItem, selectDynamicRecord, shortHexId, sortItems, spanCoversDay, stringSortValue, whenMatches, ymdKey };
739
739
 
740
740
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../../rolldown-runtime-D6vf50IK.cjs");
3
- const require_server = require("../../../server-U2d7Fb1h.cjs");
3
+ const require_server = require("../../../server-G6GtOdaW.cjs");
4
4
  const require_collection_paths = require("../../paths.cjs");
5
5
  const require_types = require("../../../types-CNqkLT4M.cjs");
6
6
  const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
@@ -1,4 +1,4 @@
1
- import { K as collectionsRegistriesConfigPath, L as writeFileAtomic, W as safeRecordId, Y as log, _ as acceptParsedSchema, g as CollectionSchemaZ, m as errorMessage, p as ONE_SECOND_MS, y as loadCollection } from "../../../server-BDxrLT41.js";
1
+ import { G as safeRecordId, R as writeFileAtomic, X as log, _ as CollectionSchemaZ, b as loadCollection, m as errorMessage, p as ONE_SECOND_MS, q as collectionsRegistriesConfigPath, v as acceptParsedSchema } from "../../../server-DKXXFTbw.js";
2
2
  import { isSafeActionTemplatePath } from "../../paths.js";
3
3
  import { n as parseRegistryIndex, r as isRecord, t as OFFICIAL_REGISTRY_NAME } from "../../../types-BKVZrtyc.js";
4
4
  import { claudeSkillDir, dataSkillDir, mirrorSkillWrite } from "../../../skill-bridge/index.js";
@@ -192,6 +192,57 @@ export declare const CollectionSchemaZ: z.ZodObject<{
192
192
  role: z.ZodString;
193
193
  template: z.ZodString;
194
194
  }, z.core.$strip>], "kind">>;
195
+ dynamicIcon: z.ZodOptional<z.ZodObject<{
196
+ source: z.ZodObject<{
197
+ collection: z.ZodString;
198
+ from: z.ZodOptional<z.ZodEnum<{
199
+ latest: "latest";
200
+ first: "first";
201
+ when: "when";
202
+ }>>;
203
+ orderBy: z.ZodOptional<z.ZodString>;
204
+ where: z.ZodOptional<z.ZodArray<z.ZodObject<{
205
+ field: z.ZodString;
206
+ op: z.ZodEnum<{
207
+ eq: "eq";
208
+ ne: "ne";
209
+ in: "in";
210
+ gt: "gt";
211
+ gte: "gte";
212
+ lt: "lt";
213
+ lte: "lte";
214
+ contains: "contains";
215
+ }>;
216
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
217
+ valueFrom: z.ZodOptional<z.ZodObject<{
218
+ record: z.ZodOptional<z.ZodString>;
219
+ field: z.ZodString;
220
+ }, z.core.$strip>>;
221
+ }, z.core.$strip>>>;
222
+ }, z.core.$strip>;
223
+ rules: z.ZodArray<z.ZodObject<{
224
+ where: z.ZodArray<z.ZodObject<{
225
+ field: z.ZodString;
226
+ op: z.ZodEnum<{
227
+ eq: "eq";
228
+ ne: "ne";
229
+ in: "in";
230
+ gt: "gt";
231
+ gte: "gte";
232
+ lt: "lt";
233
+ lte: "lte";
234
+ contains: "contains";
235
+ }>;
236
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
237
+ valueFrom: z.ZodOptional<z.ZodObject<{
238
+ record: z.ZodOptional<z.ZodString>;
239
+ field: z.ZodString;
240
+ }, z.core.$strip>>;
241
+ }, z.core.$strip>>;
242
+ icon: z.ZodString;
243
+ }, z.core.$strip>>;
244
+ fallback: z.ZodOptional<z.ZodString>;
245
+ }, z.core.$strip>>;
195
246
  }, z.core.$strip>;
196
247
  /** Result of the post-Zod acceptance gates: the resolved record dir on
197
248
  * success, or a one-line reason discovery would skip the schema. */
@@ -0,0 +1,10 @@
1
+ import { DiscoveryOptions } from './discovery';
2
+ import { LoadedCollection } from './discoveredCollection';
3
+ /** Compute the effective launcher icon for `collection`: its static
4
+ * `schema.icon` when it declares no `dynamicIcon`, else the icon
5
+ * resolved from `dynamicIcon.source`'s RAW stored records (no
6
+ * derive/enrich — the icon rules match against stored values) via the
7
+ * pure resolver. Fails soft on any read/discovery error (missing source
8
+ * collection, filesystem error): falls back to `dynamicIcon.fallback ??
9
+ * schema.icon` rather than surfacing to the collections list. */
10
+ export declare function computeCollectionIcon(collection: LoadedCollection, opts?: DiscoveryOptions): Promise<string>;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_server = require("../../server-U2d7Fb1h.cjs");
2
+ const require_server = require("../../server-G6GtOdaW.cjs");
3
3
  const require_collection_paths = require("../paths.cjs");
4
4
  exports.COMPUTED_TYPES = require_server.COMPUTED_TYPES;
5
5
  exports.CollectionSchemaZ = require_server.CollectionSchemaZ;
@@ -8,6 +8,7 @@ exports.acceptParsedSchema = require_server.acceptParsedSchema;
8
8
  exports.advanceTriggerDate = require_server.advanceTriggerDate;
9
9
  exports.buildActionSeedPrompt = require_server.buildActionSeedPrompt;
10
10
  exports.buildCollectionActionSeedPrompt = require_server.buildCollectionActionSeedPrompt;
11
+ exports.computeCollectionIcon = require_server.computeCollectionIcon;
11
12
  exports.computeSuccessor = require_server.computeSuccessor;
12
13
  exports.configureCollectionHost = require_server.configureCollectionHost;
13
14
  exports.daysInMonth = require_server.daysInMonth;
@@ -6,6 +6,7 @@ export * from './io';
6
6
  export * from './validate';
7
7
  export * from './discovery';
8
8
  export * from './derive';
9
+ export * from './dynamicIcon';
9
10
  export * from './spawn';
10
11
  export * from './delete';
11
12
  export * from './views';
@@ -1,3 +1,3 @@
1
- import { A as promptPathsFor, B as isContainedInWorkspace, C as validateCollectionRecords, D as deleteItem, E as buildCollectionActionSeedPrompt, F as resolveCreateItemId, G as safeSlugName, H as resolveDataDir, I as writeItem, J as getWorkspaceRoot, M as readCustomViewI18n, N as readItem, O as generateItemId, P as readSkillTemplate, R as SCHEMA_FILE, S as COMPUTED_TYPES, T as buildActionSeedPrompt, U as resolveTemplatePath, V as itemFilePath, W as safeRecordId, X as publishCollectionChange, Y as log, Z as setCollectionChangePublisher, _ as acceptParsedSchema, a as computeSuccessor, b as toDetail, c as isTriggerDue, d as resolveEvery, f as successorId, g as CollectionSchemaZ, h as enrichItems, i as advanceTriggerDate, j as readCustomViewHtml, k as listItems, l as maybeSpawnSuccessor, n as deleteCollection, o as daysInMonth, q as configureCollectionHost, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as discoverCollections, w as validateRecordObject, x as toSummary, y as loadCollection, z as isContainedInRoot } from "../../server-BDxrLT41.js";
1
+ import { A as listItems, B as isContainedInRoot, C as COMPUTED_TYPES, D as buildCollectionActionSeedPrompt, E as buildActionSeedPrompt, F as readSkillTemplate, G as safeRecordId, H as itemFilePath, I as resolveCreateItemId, J as configureCollectionHost, K as safeSlugName, L as writeItem, M as readCustomViewHtml, N as readCustomViewI18n, O as deleteItem, P as readItem, Q as setCollectionChangePublisher, S as toSummary, T as validateRecordObject, U as resolveDataDir, V as isContainedInWorkspace, W as resolveTemplatePath, X as log, Y as getWorkspaceRoot, Z as publishCollectionChange, _ as CollectionSchemaZ, a as computeSuccessor, b as loadCollection, c as isTriggerDue, d as resolveEvery, f as successorId, g as enrichItems, h as computeCollectionIcon, i as advanceTriggerDate, j as promptPathsFor, k as generateItemId, l as maybeSpawnSuccessor, n as deleteCollection, o as daysInMonth, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as acceptParsedSchema, w as validateCollectionRecords, x as toDetail, y as discoverCollections, z as SCHEMA_FILE } from "../../server-DKXXFTbw.js";
2
2
  import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
- export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
3
+ export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeCollectionIcon, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_collection_index = require("../collection/index.cjs");
3
- const require_server = require("../server-U2d7Fb1h.cjs");
3
+ const require_server = require("../server-G6GtOdaW.cjs");
4
4
  const require_notifier = require("../notifier-bS8IEeLA.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_fs_promises = require("node:fs/promises");
@@ -1,5 +1,5 @@
1
1
  import { whenMatches } from "../collection/index.js";
2
- import { N as readItem, c as isTriggerDue, k as listItems, l as maybeSpawnSuccessor, v as discoverCollections, y as loadCollection } from "../server-BDxrLT41.js";
2
+ import { A as listItems, P as readItem, b as loadCollection, c as isTriggerDue, l as maybeSpawnSuccessor, y as discoverCollections } from "../server-DKXXFTbw.js";
3
3
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
4
4
  import { watch } from "node:fs";
5
5
  import { mkdir } from "node:fs/promises";
@@ -38,6 +38,130 @@ function embedTargetId(field, record) {
38
38
  return "";
39
39
  }
40
40
  //#endregion
41
+ //#region src/collection/core/where.ts
42
+ /** True when `record[field]` is absent (`undefined`/`null`) — the only case
43
+ * where `ne` and every other op disagree on the result. */
44
+ function isMissing(raw) {
45
+ return raw === void 0 || raw === null;
46
+ }
47
+ /** The effective comparison value for `cond`: its literal `value`, or — for
48
+ * a `valueFrom` reference — the target field read out of `recordsById`.
49
+ * `undefined` means UNRESOLVED (no such record, or the field on it is
50
+ * missing); the caller must treat that as "never matches", not as a
51
+ * literal `undefined` value to compare against. */
52
+ function resolveValue(cond, record, recordsById) {
53
+ if (!cond.valueFrom) return cond.value;
54
+ const { record: refRecord, field } = cond.valueFrom;
55
+ const raw = (refRecord === void 0 ? record : recordsById[refRecord])?.[field];
56
+ return isMissing(raw) ? void 0 : String(raw);
57
+ }
58
+ function matchesNumericOp(operator, left, right) {
59
+ if (operator === "gt") return left > right;
60
+ if (operator === "gte") return left >= right;
61
+ if (operator === "lt") return left < right;
62
+ return left <= right;
63
+ }
64
+ /** `Number("")` / `Number(" ")` are `0`, not `NaN`, so treat a blank string
65
+ * as non-numeric explicitly — an empty field must fail a numeric compare,
66
+ * not read as zero. */
67
+ function toNumber(raw) {
68
+ return raw.trim() === "" ? NaN : Number(raw);
69
+ }
70
+ function matchesNumeric(operator, raw, value) {
71
+ if (Array.isArray(value)) return false;
72
+ const left = toNumber(raw);
73
+ const right = toNumber(value);
74
+ if (Number.isNaN(left) || Number.isNaN(right)) return false;
75
+ return matchesNumericOp(operator, left, right);
76
+ }
77
+ /** True when the present string `raw` satisfies `operator` against the
78
+ * resolved `value` (field known to exist — MISSING is handled by the
79
+ * caller before this runs, and an UNRESOLVED `valueFrom` never reaches
80
+ * here either). */
81
+ function matchesPresent(operator, raw, value) {
82
+ switch (operator) {
83
+ case "eq": return raw === String(value);
84
+ case "ne": return raw !== String(value);
85
+ case "in": return Array.isArray(value) && value.includes(raw);
86
+ case "contains": return raw.includes(String(value));
87
+ case "gt":
88
+ case "gte":
89
+ case "lt":
90
+ case "lte": return matchesNumeric(operator, raw, value);
91
+ default: return false;
92
+ }
93
+ }
94
+ /** True when `record` satisfies one condition, given `recordsById` to
95
+ * resolve a `valueFrom` reference. Two independent MISSING cases, checked
96
+ * in order:
97
+ * - `record[cond.field]` absent (`undefined`/`null`) → matches only `ne`
98
+ * (vacuously true — "not equal to X" holds when there's no value at
99
+ * all); every other op is false. Unchanged from the literal-`value`
100
+ * behaviour, regardless of whether `valueFrom` would also resolve.
101
+ * - the resolved comparison value is UNRESOLVED (a `valueFrom` whose
102
+ * target record/field doesn't exist) → false for EVERY op, including
103
+ * `ne` — a broken reference must never spuriously match. */
104
+ function matchesCond(cond, record, recordsById) {
105
+ const raw = record[cond.field];
106
+ if (isMissing(raw)) return cond.op === "ne";
107
+ const value = resolveValue(cond, record, recordsById);
108
+ if (value === void 0) return false;
109
+ return matchesPresent(cond.op, String(raw), value);
110
+ }
111
+ /** True when `record` satisfies every condition in `where` (AND). An empty
112
+ * `where` matches everything. `recordsById` — the source collection's
113
+ * records keyed by primaryKey — resolves any `valueFrom` reference;
114
+ * omitted (default `{}`) for callers with no cross-record lookups, in
115
+ * which case every `valueFrom` condition is UNRESOLVED and so never
116
+ * matches. */
117
+ function matchesWhere(where, record, recordsById = {}) {
118
+ return where.every((cond) => matchesCond(cond, record, recordsById));
119
+ }
120
+ //#endregion
121
+ //#region src/collection/core/dynamicIcon.ts
122
+ /** The record with the greatest `String(record[field])` (localeCompare) —
123
+ * ties keep the first-seen record (stable left-to-right `reduce`). */
124
+ function latestByField(pool, field) {
125
+ return pool.reduce((latest, candidate) => String(candidate[field] ?? "").localeCompare(String(latest[field] ?? "")) > 0 ? candidate : latest);
126
+ }
127
+ /** Reduce `records` to the one record that decides the effective icon, per
128
+ * `source`'s `where` filter + `from` strategy:
129
+ * - pool = `source.where`-filtered records, or every record when unset;
130
+ * - an empty pool resolves to `null` (no source record → fallback);
131
+ * - `from: "first"` / `"when"` → the first pool record (storage order);
132
+ * - `from: "latest"` (default), with `orderBy` given → the pool record
133
+ * whose `String(record[orderBy])` sorts highest;
134
+ * - `from: "latest"`, with no `orderBy` → the last pool record.
135
+ * `recordsById` (the source collection's records keyed by primaryKey)
136
+ * resolves any `valueFrom` reference inside `source.where`; omitted for
137
+ * callers with no cross-record lookups. */
138
+ function selectDynamicRecord(records, source, orderBy, recordsById = {}) {
139
+ const { where } = source;
140
+ const pool = where ? records.filter((record) => matchesWhere(where, record, recordsById)) : records;
141
+ if (pool.length === 0) return null;
142
+ if (source.from === "first" || source.from === "when") return pool[0];
143
+ return orderBy ? latestByField(pool, orderBy) : pool[pool.length - 1];
144
+ }
145
+ /** Map a resolved source record to the effective icon: `spec.fallback`
146
+ * (or the collection's own static `icon`) when there's no record or no
147
+ * rule matches; otherwise the `icon` of the first rule whose `where`
148
+ * matches the record. `recordsById` resolves any `valueFrom` reference
149
+ * inside a rule's `where`, same as `selectDynamicRecord`. */
150
+ function resolveIcon(record, spec, staticIcon, recordsById = {}) {
151
+ const fallback = spec.fallback ?? staticIcon;
152
+ if (!record) return fallback;
153
+ const matched = spec.rules.find((rule) => matchesWhere(rule.where, record, recordsById));
154
+ return matched ? matched.icon : fallback;
155
+ }
156
+ var isDateLikeField = (field) => field.type === "date" || field.type === "datetime";
157
+ /** The first field key (declaration order) whose type is `date` or
158
+ * `datetime` — the default `orderBy` for `from: "latest"` when a
159
+ * `DynamicIconSource` doesn't name one. `undefined` when the schema has
160
+ * no date-like field. */
161
+ function firstDateField(schema) {
162
+ return Object.entries(schema.fields).find(([, field]) => isDateLikeField(field))?.[0];
163
+ }
164
+ //#endregion
41
165
  //#region src/collection/core/derivedFormula.ts
42
166
  function evaluateDerived(formula, ctx) {
43
167
  let tokens;
@@ -413,17 +537,41 @@ Object.defineProperty(exports, "evaluateDerived", {
413
537
  return evaluateDerived;
414
538
  }
415
539
  });
540
+ Object.defineProperty(exports, "firstDateField", {
541
+ enumerable: true,
542
+ get: function() {
543
+ return firstDateField;
544
+ }
545
+ });
416
546
  Object.defineProperty(exports, "isFieldDrivenEvery", {
417
547
  enumerable: true,
418
548
  get: function() {
419
549
  return isFieldDrivenEvery;
420
550
  }
421
551
  });
552
+ Object.defineProperty(exports, "matchesWhere", {
553
+ enumerable: true,
554
+ get: function() {
555
+ return matchesWhere;
556
+ }
557
+ });
558
+ Object.defineProperty(exports, "resolveIcon", {
559
+ enumerable: true,
560
+ get: function() {
561
+ return resolveIcon;
562
+ }
563
+ });
422
564
  Object.defineProperty(exports, "resolveRowRefs", {
423
565
  enumerable: true,
424
566
  get: function() {
425
567
  return resolveRowRefs;
426
568
  }
427
569
  });
570
+ Object.defineProperty(exports, "selectDynamicRecord", {
571
+ enumerable: true,
572
+ get: function() {
573
+ return selectDynamicRecord;
574
+ }
575
+ });
428
576
 
429
- //# sourceMappingURL=deriveAll-Cb9rWjan.cjs.map
577
+ //# sourceMappingURL=deriveAll-BQ_p5HSB.cjs.map