@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.
- package/dist/core/schema.d.ts +24 -2
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.js +2 -2
- package/dist/{schema-LrmrgW12.cjs → schema-5P5ZJPrY.cjs} +11 -1
- package/dist/schema-5P5ZJPrY.cjs.map +1 -0
- package/dist/{schema-DyfSfVzh.js → schema-BcnitlL8.js} +6 -2
- package/dist/schema-BcnitlL8.js.map +1 -0
- package/dist/server/discovery.d.ts +14 -2
- package/dist/server/discovery.d.ts.map +1 -1
- package/dist/server/host.d.ts +25 -0
- package/dist/server/host.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/io.d.ts +6 -0
- package/dist/server/io.d.ts.map +1 -1
- package/dist/server/spawn.d.ts +8 -1
- package/dist/server/spawn.d.ts.map +1 -1
- package/dist/server.cjs +117 -17
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +115 -18
- package/dist/server.js.map +1 -1
- package/dist/style.css +3 -3
- package/dist/vue/components/CollectionCustomView.vue.d.ts +19 -1
- package/dist/vue/components/CollectionCustomView.vue.d.ts.map +1 -1
- package/dist/vue/components/CollectionRecordPanel.vue.d.ts +2 -2
- package/dist/vue/components/CollectionView.vue.d.ts.map +1 -1
- package/dist/vue/uiContext.d.ts +12 -0
- package/dist/vue/uiContext.d.ts.map +1 -1
- package/dist/vue.cjs +144 -4
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +145 -5
- package/dist/vue.js.map +1 -1
- package/package.json +2 -2
- package/dist/schema-DyfSfVzh.js.map +0 -1
- 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-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|