@mulmoclaude/collection-plugin 0.5.2 → 0.5.6

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 (36) hide show
  1. package/dist/core/schema.d.ts +24 -2
  2. package/dist/core/schema.d.ts.map +1 -1
  3. package/dist/index.cjs +2 -1
  4. package/dist/index.js +2 -2
  5. package/dist/{schema-LrmrgW12.cjs → schema-5P5ZJPrY.cjs} +11 -1
  6. package/dist/schema-5P5ZJPrY.cjs.map +1 -0
  7. package/dist/{schema-DyfSfVzh.js → schema-BcnitlL8.js} +6 -2
  8. package/dist/schema-BcnitlL8.js.map +1 -0
  9. package/dist/server/discovery.d.ts +14 -2
  10. package/dist/server/discovery.d.ts.map +1 -1
  11. package/dist/server/host.d.ts +25 -0
  12. package/dist/server/host.d.ts.map +1 -1
  13. package/dist/server/index.d.ts +1 -1
  14. package/dist/server/index.d.ts.map +1 -1
  15. package/dist/server/io.d.ts +6 -0
  16. package/dist/server/io.d.ts.map +1 -1
  17. package/dist/server/spawn.d.ts +8 -1
  18. package/dist/server/spawn.d.ts.map +1 -1
  19. package/dist/server.cjs +117 -17
  20. package/dist/server.cjs.map +1 -1
  21. package/dist/server.js +115 -18
  22. package/dist/server.js.map +1 -1
  23. package/dist/style.css +3 -3
  24. package/dist/vue/components/CollectionCustomView.vue.d.ts +19 -1
  25. package/dist/vue/components/CollectionCustomView.vue.d.ts.map +1 -1
  26. package/dist/vue/components/CollectionRecordPanel.vue.d.ts +2 -2
  27. package/dist/vue/components/CollectionView.vue.d.ts.map +1 -1
  28. package/dist/vue/uiContext.d.ts +12 -0
  29. package/dist/vue/uiContext.d.ts.map +1 -1
  30. package/dist/vue.cjs +144 -4
  31. package/dist/vue.cjs.map +1 -1
  32. package/dist/vue.js +145 -5
  33. package/dist/vue.js.map +1 -1
  34. package/package.json +2 -2
  35. package/dist/schema-DyfSfVzh.js.map +0 -1
  36. package/dist/schema-LrmrgW12.cjs.map +0 -1
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as INGEST_KINDS, t as FEED_SCHEDULES } from "./schema-DyfSfVzh.js";
1
+ import { n as INGEST_KINDS, r as isFieldDrivenEvery, t as FEED_SCHEDULES } from "./schema-BcnitlL8.js";
2
2
  import { t as deriveAll } from "./deriveAll-BHcs1erT.js";
3
3
  import path from "node:path";
4
4
  import { promises, realpathSync } from "node:fs";
@@ -7,6 +7,7 @@ import { randomBytes, randomUUID } from "node:crypto";
7
7
  import { z } from "zod";
8
8
  //#region src/server/host.ts
9
9
  var current = null;
10
+ var changePublisher = null;
10
11
  /** Wire the engine to a host. Call once at server startup, before any
11
12
  * collection storage operation. Re-binding to a *different* host throws —
12
13
  * silently redirecting later filesystem operations to another workspace
@@ -15,6 +16,22 @@ function configureCollectionHost(host) {
15
16
  if (current !== null && current !== host) throw new Error("@mulmoclaude/collection-plugin/server: configureCollectionHost() was already called with a different host");
16
17
  current = host;
17
18
  }
19
+ /** Wire a publisher that broadcasts record-change events; the host bridges it
20
+ * to its pubsub. Kept SEPARATE from `configureCollectionHost` because the
21
+ * host's pubsub instance isn't ready at host-binding time (the binding is set
22
+ * at the top of server startup, the pubsub later). Optional: left unset, every
23
+ * write is silent — the default for tests and for a host that doesn't want
24
+ * live view updates. Pass `null` to detach (test teardown). */
25
+ function setCollectionChangePublisher(publish) {
26
+ changePublisher = publish;
27
+ }
28
+ /** Broadcast a record-change event if a publisher is wired (no-op otherwise).
29
+ * Called from the write path (`writeItem`/`deleteItem`). The wired publisher is
30
+ * expected to be fire-and-forget (it wraps its own pubsub call in try/catch),
31
+ * so this stays a thin pass-through and never throws into the write. */
32
+ function publishCollectionChange(payload) {
33
+ changePublisher?.(payload);
34
+ }
18
35
  function requireHost() {
19
36
  if (current === null) throw new Error("@mulmoclaude/collection-plugin/server: configureCollectionHost() was not called by the host");
20
37
  return current;
@@ -391,13 +408,12 @@ async function writeItem(dataDir, itemId, item, opts = {}) {
391
408
  } finally {
392
409
  await handle.close();
393
410
  }
394
- return {
395
- kind: "ok",
396
- itemId: safeId,
397
- item
398
- };
399
- }
400
- await writeFileAtomic(filePath, payload);
411
+ } else await writeFileAtomic(filePath, payload);
412
+ if (opts.slug) publishCollectionChange({
413
+ slug: opts.slug,
414
+ ids: [safeId],
415
+ op: "upsert"
416
+ });
401
417
  return {
402
418
  kind: "ok",
403
419
  itemId: safeId,
@@ -423,6 +439,11 @@ async function deleteItem(dataDir, itemId, opts = {}) {
423
439
  const filePath = itemFilePath(dataDir, safeId);
424
440
  try {
425
441
  await unlink(filePath);
442
+ if (opts.slug) publishCollectionChange({
443
+ slug: opts.slug,
444
+ ids: [safeId],
445
+ op: "delete"
446
+ });
426
447
  return {
427
448
  kind: "ok",
428
449
  itemId: safeId
@@ -777,7 +798,7 @@ var CustomViewSchema = z.object({
777
798
  file: z.string().trim().min(1).refine(isSafeCustomViewPath, "must be a safe path under `views/` ending in `.html` (e.g. `views/year.html`; no `..`, no leading `/`, no backslash)"),
778
799
  capabilities: z.array(z.enum(["read", "write"])).optional()
779
800
  });
780
- var EverySchema = z.object({
801
+ var EveryLiteralSchema = z.object({
781
802
  unit: z.enum([
782
803
  "day",
783
804
  "week",
@@ -786,7 +807,12 @@ var EverySchema = z.object({
786
807
  ]),
787
808
  interval: z.number().int().min(1),
788
809
  dayOfMonth: z.union([z.number().int().min(1).max(31), z.literal("last")]).optional()
789
- });
810
+ }).strict();
811
+ var EveryFieldDrivenSchema = z.object({
812
+ fromField: z.string().trim().min(1),
813
+ map: z.record(z.string(), EveryLiteralSchema)
814
+ }).strict();
815
+ var EverySchema = z.union([EveryLiteralSchema, EveryFieldDrivenSchema]);
790
816
  var SpawnSchema = z.object({
791
817
  when: WhenSchema.optional(),
792
818
  every: EverySchema,
@@ -828,6 +854,36 @@ function spawnSuccessorStartsInert(schema) {
828
854
  if (spawn.set && Object.prototype.hasOwnProperty.call(spawn.set, field)) return !values.includes(String(spawn.set[field]));
829
855
  return !(spawn.carry ?? []).includes(field);
830
856
  }
857
+ function fieldDrivenSpawnEvery(schema) {
858
+ const every = schema.spawn?.every;
859
+ if (!every || !isFieldDrivenEvery(every)) return null;
860
+ return every;
861
+ }
862
+ function fieldDrivenFromFieldIsEnum(schema) {
863
+ const driven = fieldDrivenSpawnEvery(schema);
864
+ if (!driven) return true;
865
+ return schema.fields[driven.fromField]?.type === "enum";
866
+ }
867
+ function fieldDrivenMapCoversValues(schema) {
868
+ const driven = fieldDrivenSpawnEvery(schema);
869
+ if (!driven) return true;
870
+ const target = schema.fields[driven.fromField];
871
+ if (!target || target.type !== "enum" || target.values === void 0) return true;
872
+ const values = new Set(target.values);
873
+ const keys = Object.keys(driven.map);
874
+ return keys.length === values.size && keys.every((key) => values.has(key));
875
+ }
876
+ function fieldDrivenFromFieldCarried(schema) {
877
+ const driven = fieldDrivenSpawnEvery(schema);
878
+ if (!driven) return true;
879
+ const { carry, set } = schema.spawn ?? {};
880
+ if (set && Object.prototype.hasOwnProperty.call(set, driven.fromField)) {
881
+ const raw = set[driven.fromField];
882
+ if (raw === void 0 || raw === null || raw === "") return false;
883
+ return Object.prototype.hasOwnProperty.call(driven.map, String(raw));
884
+ }
885
+ return (carry ?? []).includes(driven.fromField);
886
+ }
831
887
  var IngestSchemaZ = z.object({
832
888
  kind: z.enum(INGEST_KINDS),
833
889
  url: z.string().url(),
@@ -904,6 +960,15 @@ var CollectionSchemaZ = z.object({
904
960
  }).refine((schema) => spawnSuccessorStartsInert(schema), {
905
961
  message: "`spawn` must leave the successor in a non-matching state (e.g. `set` the status to a pending value); seeding the predicate field to a matching value via `set`/`carry` would respawn forever",
906
962
  path: ["spawn"]
963
+ }).refine((schema) => fieldDrivenFromFieldIsEnum(schema), {
964
+ message: "`spawn.every.fromField` must name a top-level `enum` field declared in `fields`",
965
+ path: ["spawn"]
966
+ }).refine((schema) => fieldDrivenMapCoversValues(schema), {
967
+ message: "`spawn.every.map` keys must exactly cover the `values` of the `enum` named by `fromField` (no missing or extra keys)",
968
+ path: ["spawn"]
969
+ }).refine((schema) => fieldDrivenFromFieldCarried(schema), {
970
+ message: "`spawn.every.fromField` must appear in `spawn.carry`, or be written by `spawn.set` to a value present in `spawn.every.map`, so the successor keeps a resolvable recurrence interval",
971
+ path: ["spawn"]
907
972
  }).refine((schema) => schema.calendarField === void 0 || isDateLike(schema.fields[schema.calendarField]?.type), {
908
973
  message: "schema `calendarField` must name a top-level `date` or `datetime` field declared in `fields`",
909
974
  path: ["calendarField"]
@@ -1310,6 +1375,18 @@ function matchesWhen(when, schema, item) {
1310
1375
  const raw = item[completionField];
1311
1376
  return raw !== void 0 && raw !== null && completionDoneValues.includes(String(raw));
1312
1377
  }
1378
+ /** Resolve the literal `every` that applies to `sourceItem`. Literal-arm
1379
+ * `spawn.every` passes through unchanged. Field-driven `spawn.every` reads
1380
+ * `sourceItem[fromField]` and looks it up in `map`; an empty field or a
1381
+ * value with no map entry yields null (caller skips + logs — see plan §5).
1382
+ * Discovery rejects a map that doesn't cover the enum's values, so null
1383
+ * here means a record that predates a map/enum edit. */
1384
+ function resolveEvery(every, sourceItem) {
1385
+ if (!isFieldDrivenEvery(every)) return every;
1386
+ const raw = sourceItem[every.fromField];
1387
+ if (raw === void 0 || raw === null || raw === "") return null;
1388
+ return every.map[String(raw)] ?? null;
1389
+ }
1313
1390
  /** Build the successor record purely from (schema, source record, source
1314
1391
  * id). Returns null when the schema has no spawn/triggerField or the
1315
1392
  * source's trigger date is unparseable. */
@@ -1318,7 +1395,9 @@ function computeSuccessor(schema, sourceItem, sourceId) {
1318
1395
  if (!spawn || !triggerField) return null;
1319
1396
  const srcDate = parseCivil(sourceItem[triggerField]);
1320
1397
  if (srcDate === null) return null;
1321
- const next = advanceTriggerDate(srcDate, spawn.every);
1398
+ const every = resolveEvery(spawn.every, sourceItem);
1399
+ if (every === null) return null;
1400
+ const next = advanceTriggerDate(srcDate, every);
1322
1401
  const nextId = successorId(sourceId, next);
1323
1402
  const record = {};
1324
1403
  for (const field of spawn.carry ?? []) if (Object.prototype.hasOwnProperty.call(sourceItem, field)) record[field] = sourceItem[field];
@@ -1330,6 +1409,27 @@ function computeSuccessor(schema, sourceItem, sourceId) {
1330
1409
  record
1331
1410
  };
1332
1411
  }
1412
+ /** Warn precisely about which of `computeSuccessor`'s two null causes fired
1413
+ * (plan §5): an unparseable source trigger date (the original cause), or a
1414
+ * field-driven `every` whose record value has no `map` entry (a record that
1415
+ * predates a map/enum edit — discovery rejects this statically otherwise). */
1416
+ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
1417
+ if (parseCivil(sourceItem[triggerField]) === null) {
1418
+ log.warn("collections", "spawn skipped: source trigger date unparseable", {
1419
+ slug,
1420
+ sourceId,
1421
+ triggerField
1422
+ });
1423
+ return;
1424
+ }
1425
+ const fromField = isFieldDrivenEvery(every) ? every.fromField : void 0;
1426
+ log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
1427
+ slug,
1428
+ sourceId,
1429
+ fromField,
1430
+ value: fromField === void 0 ? void 0 : sourceItem[fromField]
1431
+ });
1432
+ }
1333
1433
  /** Idempotently create the successor for `sourceItem` when it matches the
1334
1434
  * spawn predicate. No-op when the schema declares no spawn, the
1335
1435
  * predicate doesn't match, the trigger date is unparseable, or the
@@ -1341,11 +1441,7 @@ async function maybeSpawnSuccessor(slug, schema, dataDir, sourceItem, sourceId,
1341
1441
  if (!matchesWhen(spawn.when, schema, sourceItem)) return;
1342
1442
  const computed = computeSuccessor(schema, sourceItem, sourceId);
1343
1443
  if (computed === null) {
1344
- log.warn("collections", "spawn skipped: source trigger date unparseable", {
1345
- slug,
1346
- sourceId,
1347
- triggerField: schema.triggerField
1348
- });
1444
+ logSpawnSkip(slug, schema.triggerField, spawn.every, sourceItem, sourceId);
1349
1445
  return;
1350
1446
  }
1351
1447
  if (matchesWhen(spawn.when, schema, computed.record)) {
@@ -1359,7 +1455,8 @@ async function maybeSpawnSuccessor(slug, schema, dataDir, sourceItem, sourceId,
1359
1455
  try {
1360
1456
  const result = await writeItem(dataDir, computed.id, computed.record, {
1361
1457
  ...ioOpts,
1362
- refuseOverwrite: true
1458
+ refuseOverwrite: true,
1459
+ slug
1363
1460
  });
1364
1461
  if (result.kind === "ok") log.info("collections", "spawned successor", {
1365
1462
  slug,
@@ -1607,6 +1704,6 @@ async function deleteCustomView(collection, viewId, opts = {}) {
1607
1704
  };
1608
1705
  }
1609
1706
  //#endregion
1610
- 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, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, readCustomViewHtml, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveTemplatePath, safeRecordId, safeSlugName, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
1707
+ 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, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, publishCollectionChange, readCustomViewHtml, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
1611
1708
 
1612
1709
  //# sourceMappingURL=server.js.map