@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.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-CKQMccvm.cjs");
3
- const require_schema = require("./schema-LrmrgW12.cjs");
3
+ const require_schema = require("./schema-5P5ZJPrY.cjs");
4
4
  const require_deriveAll = require("./deriveAll-BJ0Lvm1Q.cjs");
5
5
  let node_path = require("node:path");
6
6
  node_path = require_chunk.__toESM(node_path, 1);
@@ -10,6 +10,7 @@ let node_crypto = require("node:crypto");
10
10
  let zod = require("zod");
11
11
  //#region src/server/host.ts
12
12
  var current = null;
13
+ var changePublisher = null;
13
14
  /** Wire the engine to a host. Call once at server startup, before any
14
15
  * collection storage operation. Re-binding to a *different* host throws —
15
16
  * silently redirecting later filesystem operations to another workspace
@@ -18,6 +19,22 @@ function configureCollectionHost(host) {
18
19
  if (current !== null && current !== host) throw new Error("@mulmoclaude/collection-plugin/server: configureCollectionHost() was already called with a different host");
19
20
  current = host;
20
21
  }
22
+ /** Wire a publisher that broadcasts record-change events; the host bridges it
23
+ * to its pubsub. Kept SEPARATE from `configureCollectionHost` because the
24
+ * host's pubsub instance isn't ready at host-binding time (the binding is set
25
+ * at the top of server startup, the pubsub later). Optional: left unset, every
26
+ * write is silent — the default for tests and for a host that doesn't want
27
+ * live view updates. Pass `null` to detach (test teardown). */
28
+ function setCollectionChangePublisher(publish) {
29
+ changePublisher = publish;
30
+ }
31
+ /** Broadcast a record-change event if a publisher is wired (no-op otherwise).
32
+ * Called from the write path (`writeItem`/`deleteItem`). The wired publisher is
33
+ * expected to be fire-and-forget (it wraps its own pubsub call in try/catch),
34
+ * so this stays a thin pass-through and never throws into the write. */
35
+ function publishCollectionChange(payload) {
36
+ changePublisher?.(payload);
37
+ }
21
38
  function requireHost() {
22
39
  if (current === null) throw new Error("@mulmoclaude/collection-plugin/server: configureCollectionHost() was not called by the host");
23
40
  return current;
@@ -394,13 +411,12 @@ async function writeItem(dataDir, itemId, item, opts = {}) {
394
411
  } finally {
395
412
  await handle.close();
396
413
  }
397
- return {
398
- kind: "ok",
399
- itemId: safeId,
400
- item
401
- };
402
- }
403
- await writeFileAtomic(filePath, payload);
414
+ } else await writeFileAtomic(filePath, payload);
415
+ if (opts.slug) publishCollectionChange({
416
+ slug: opts.slug,
417
+ ids: [safeId],
418
+ op: "upsert"
419
+ });
404
420
  return {
405
421
  kind: "ok",
406
422
  itemId: safeId,
@@ -426,6 +442,11 @@ async function deleteItem(dataDir, itemId, opts = {}) {
426
442
  const filePath = itemFilePath(dataDir, safeId);
427
443
  try {
428
444
  await (0, node_fs_promises.unlink)(filePath);
445
+ if (opts.slug) publishCollectionChange({
446
+ slug: opts.slug,
447
+ ids: [safeId],
448
+ op: "delete"
449
+ });
429
450
  return {
430
451
  kind: "ok",
431
452
  itemId: safeId
@@ -780,7 +801,7 @@ var CustomViewSchema = zod.z.object({
780
801
  file: zod.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)"),
781
802
  capabilities: zod.z.array(zod.z.enum(["read", "write"])).optional()
782
803
  });
783
- var EverySchema = zod.z.object({
804
+ var EveryLiteralSchema = zod.z.object({
784
805
  unit: zod.z.enum([
785
806
  "day",
786
807
  "week",
@@ -789,7 +810,12 @@ var EverySchema = zod.z.object({
789
810
  ]),
790
811
  interval: zod.z.number().int().min(1),
791
812
  dayOfMonth: zod.z.union([zod.z.number().int().min(1).max(31), zod.z.literal("last")]).optional()
792
- });
813
+ }).strict();
814
+ var EveryFieldDrivenSchema = zod.z.object({
815
+ fromField: zod.z.string().trim().min(1),
816
+ map: zod.z.record(zod.z.string(), EveryLiteralSchema)
817
+ }).strict();
818
+ var EverySchema = zod.z.union([EveryLiteralSchema, EveryFieldDrivenSchema]);
793
819
  var SpawnSchema = zod.z.object({
794
820
  when: WhenSchema.optional(),
795
821
  every: EverySchema,
@@ -831,6 +857,36 @@ function spawnSuccessorStartsInert(schema) {
831
857
  if (spawn.set && Object.prototype.hasOwnProperty.call(spawn.set, field)) return !values.includes(String(spawn.set[field]));
832
858
  return !(spawn.carry ?? []).includes(field);
833
859
  }
860
+ function fieldDrivenSpawnEvery(schema) {
861
+ const every = schema.spawn?.every;
862
+ if (!every || !require_schema.isFieldDrivenEvery(every)) return null;
863
+ return every;
864
+ }
865
+ function fieldDrivenFromFieldIsEnum(schema) {
866
+ const driven = fieldDrivenSpawnEvery(schema);
867
+ if (!driven) return true;
868
+ return schema.fields[driven.fromField]?.type === "enum";
869
+ }
870
+ function fieldDrivenMapCoversValues(schema) {
871
+ const driven = fieldDrivenSpawnEvery(schema);
872
+ if (!driven) return true;
873
+ const target = schema.fields[driven.fromField];
874
+ if (!target || target.type !== "enum" || target.values === void 0) return true;
875
+ const values = new Set(target.values);
876
+ const keys = Object.keys(driven.map);
877
+ return keys.length === values.size && keys.every((key) => values.has(key));
878
+ }
879
+ function fieldDrivenFromFieldCarried(schema) {
880
+ const driven = fieldDrivenSpawnEvery(schema);
881
+ if (!driven) return true;
882
+ const { carry, set } = schema.spawn ?? {};
883
+ if (set && Object.prototype.hasOwnProperty.call(set, driven.fromField)) {
884
+ const raw = set[driven.fromField];
885
+ if (raw === void 0 || raw === null || raw === "") return false;
886
+ return Object.prototype.hasOwnProperty.call(driven.map, String(raw));
887
+ }
888
+ return (carry ?? []).includes(driven.fromField);
889
+ }
834
890
  var IngestSchemaZ = zod.z.object({
835
891
  kind: zod.z.enum(require_schema.INGEST_KINDS),
836
892
  url: zod.z.string().url(),
@@ -907,6 +963,15 @@ var CollectionSchemaZ = zod.z.object({
907
963
  }).refine((schema) => spawnSuccessorStartsInert(schema), {
908
964
  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",
909
965
  path: ["spawn"]
966
+ }).refine((schema) => fieldDrivenFromFieldIsEnum(schema), {
967
+ message: "`spawn.every.fromField` must name a top-level `enum` field declared in `fields`",
968
+ path: ["spawn"]
969
+ }).refine((schema) => fieldDrivenMapCoversValues(schema), {
970
+ message: "`spawn.every.map` keys must exactly cover the `values` of the `enum` named by `fromField` (no missing or extra keys)",
971
+ path: ["spawn"]
972
+ }).refine((schema) => fieldDrivenFromFieldCarried(schema), {
973
+ 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",
974
+ path: ["spawn"]
910
975
  }).refine((schema) => schema.calendarField === void 0 || isDateLike(schema.fields[schema.calendarField]?.type), {
911
976
  message: "schema `calendarField` must name a top-level `date` or `datetime` field declared in `fields`",
912
977
  path: ["calendarField"]
@@ -1313,6 +1378,18 @@ function matchesWhen(when, schema, item) {
1313
1378
  const raw = item[completionField];
1314
1379
  return raw !== void 0 && raw !== null && completionDoneValues.includes(String(raw));
1315
1380
  }
1381
+ /** Resolve the literal `every` that applies to `sourceItem`. Literal-arm
1382
+ * `spawn.every` passes through unchanged. Field-driven `spawn.every` reads
1383
+ * `sourceItem[fromField]` and looks it up in `map`; an empty field or a
1384
+ * value with no map entry yields null (caller skips + logs — see plan §5).
1385
+ * Discovery rejects a map that doesn't cover the enum's values, so null
1386
+ * here means a record that predates a map/enum edit. */
1387
+ function resolveEvery(every, sourceItem) {
1388
+ if (!require_schema.isFieldDrivenEvery(every)) return every;
1389
+ const raw = sourceItem[every.fromField];
1390
+ if (raw === void 0 || raw === null || raw === "") return null;
1391
+ return every.map[String(raw)] ?? null;
1392
+ }
1316
1393
  /** Build the successor record purely from (schema, source record, source
1317
1394
  * id). Returns null when the schema has no spawn/triggerField or the
1318
1395
  * source's trigger date is unparseable. */
@@ -1321,7 +1398,9 @@ function computeSuccessor(schema, sourceItem, sourceId) {
1321
1398
  if (!spawn || !triggerField) return null;
1322
1399
  const srcDate = parseCivil(sourceItem[triggerField]);
1323
1400
  if (srcDate === null) return null;
1324
- const next = advanceTriggerDate(srcDate, spawn.every);
1401
+ const every = resolveEvery(spawn.every, sourceItem);
1402
+ if (every === null) return null;
1403
+ const next = advanceTriggerDate(srcDate, every);
1325
1404
  const nextId = successorId(sourceId, next);
1326
1405
  const record = {};
1327
1406
  for (const field of spawn.carry ?? []) if (Object.prototype.hasOwnProperty.call(sourceItem, field)) record[field] = sourceItem[field];
@@ -1333,6 +1412,27 @@ function computeSuccessor(schema, sourceItem, sourceId) {
1333
1412
  record
1334
1413
  };
1335
1414
  }
1415
+ /** Warn precisely about which of `computeSuccessor`'s two null causes fired
1416
+ * (plan §5): an unparseable source trigger date (the original cause), or a
1417
+ * field-driven `every` whose record value has no `map` entry (a record that
1418
+ * predates a map/enum edit — discovery rejects this statically otherwise). */
1419
+ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
1420
+ if (parseCivil(sourceItem[triggerField]) === null) {
1421
+ log.warn("collections", "spawn skipped: source trigger date unparseable", {
1422
+ slug,
1423
+ sourceId,
1424
+ triggerField
1425
+ });
1426
+ return;
1427
+ }
1428
+ const fromField = require_schema.isFieldDrivenEvery(every) ? every.fromField : void 0;
1429
+ log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
1430
+ slug,
1431
+ sourceId,
1432
+ fromField,
1433
+ value: fromField === void 0 ? void 0 : sourceItem[fromField]
1434
+ });
1435
+ }
1336
1436
  /** Idempotently create the successor for `sourceItem` when it matches the
1337
1437
  * spawn predicate. No-op when the schema declares no spawn, the
1338
1438
  * predicate doesn't match, the trigger date is unparseable, or the
@@ -1344,11 +1444,7 @@ async function maybeSpawnSuccessor(slug, schema, dataDir, sourceItem, sourceId,
1344
1444
  if (!matchesWhen(spawn.when, schema, sourceItem)) return;
1345
1445
  const computed = computeSuccessor(schema, sourceItem, sourceId);
1346
1446
  if (computed === null) {
1347
- log.warn("collections", "spawn skipped: source trigger date unparseable", {
1348
- slug,
1349
- sourceId,
1350
- triggerField: schema.triggerField
1351
- });
1447
+ logSpawnSkip(slug, schema.triggerField, spawn.every, sourceItem, sourceId);
1352
1448
  return;
1353
1449
  }
1354
1450
  if (matchesWhen(spawn.when, schema, computed.record)) {
@@ -1362,7 +1458,8 @@ async function maybeSpawnSuccessor(slug, schema, dataDir, sourceItem, sourceId,
1362
1458
  try {
1363
1459
  const result = await writeItem(dataDir, computed.id, computed.record, {
1364
1460
  ...ioOpts,
1365
- refuseOverwrite: true
1461
+ refuseOverwrite: true,
1462
+ slug
1366
1463
  });
1367
1464
  if (result.kind === "ok") log.info("collections", "spawned successor", {
1368
1465
  slug,
@@ -1641,14 +1738,17 @@ exports.loadCollection = loadCollection;
1641
1738
  exports.log = log;
1642
1739
  exports.maybeSpawnSuccessor = maybeSpawnSuccessor;
1643
1740
  exports.parseCivil = parseCivil;
1741
+ exports.publishCollectionChange = publishCollectionChange;
1644
1742
  exports.readCustomViewHtml = readCustomViewHtml;
1645
1743
  exports.readItem = readItem;
1646
1744
  exports.readSkillTemplate = readSkillTemplate;
1647
1745
  exports.resolveCreateItemId = resolveCreateItemId;
1648
1746
  exports.resolveDataDir = resolveDataDir;
1747
+ exports.resolveEvery = resolveEvery;
1649
1748
  exports.resolveTemplatePath = resolveTemplatePath;
1650
1749
  exports.safeRecordId = safeRecordId;
1651
1750
  exports.safeSlugName = safeSlugName;
1751
+ exports.setCollectionChangePublisher = setCollectionChangePublisher;
1652
1752
  exports.successorId = successorId;
1653
1753
  exports.toDetail = toDetail;
1654
1754
  exports.toSummary = toSummary;