@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as INGEST_KINDS, t as FEED_SCHEDULES } from "./schema-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|