@mulmoclaude/core 3.14.0 → 3.15.0
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/collection/registry/server/index.cjs +23 -6
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +24 -7
- package/dist/collection/registry/server/index.js.map +1 -1
- package/dist/collection/server/appViews.d.ts +40 -1
- package/dist/collection/server/index.cjs +2 -1
- package/dist/collection/server/index.d.ts +1 -1
- package/dist/collection/server/index.js +2 -2
- package/dist/collection/server/publishProject.d.ts +31 -3
- package/dist/collection-watchers/index.cjs +1 -1
- package/dist/collection-watchers/index.js +1 -1
- package/dist/feeds/server/index.cjs +1 -1
- package/dist/feeds/server/index.js +1 -1
- package/dist/{server-DknYbXBo.cjs → server-DLzQ4jFu.cjs} +136 -24
- package/dist/server-DLzQ4jFu.cjs.map +1 -0
- package/dist/{server-CQSF5fHe.js → server-jm7aGA3g.js} +131 -25
- package/dist/server-jm7aGA3g.js.map +1 -0
- package/package.json +1 -1
- package/dist/server-CQSF5fHe.js.map +0 -1
- package/dist/server-DknYbXBo.cjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CollectionSchema } from '../core/schema';
|
|
2
|
-
import { AppViewConfigDoc, NormalizedView, ViewAudience } from './appViews';
|
|
2
|
+
import { AppViewConfigDoc, NormalizedView, ProjectedViewCollection, ProjectedViewWrite, ViewAudience } from './appViews';
|
|
3
3
|
import { AuthoredApp, AuthoredCollectionConfig } from './publishManifest';
|
|
4
4
|
/** Who and when. Threaded in rather than read from a clock inside the
|
|
5
5
|
* projection so the projection stays pure and the tests can assert on an
|
|
@@ -296,9 +296,37 @@ export interface AppViewTier {
|
|
|
296
296
|
* and neither is the clock. What is here is the answer to "what may this
|
|
297
297
|
* audience read, and how" — computed once, so the page never has to guess and
|
|
298
298
|
* never has to discover it from a denial. */
|
|
299
|
-
|
|
299
|
+
/** What the RULES will be in force with, as against what the manifest says.
|
|
300
|
+
*
|
|
301
|
+
* `projectPublish` replaces BOTH `participantRead` and `collections` with what
|
|
302
|
+
* the staged schemas carry, so at publish the promoted pair is what decides
|
|
303
|
+
* whether a read is allowed and which transitions exist. They travel together
|
|
304
|
+
* deliberately: passing one and not the other publishes a page whose datasets
|
|
305
|
+
* follow revision A and whose buttons follow revision B.
|
|
306
|
+
*
|
|
307
|
+
* At DEPLOY the manifest is exactly what is being staged, so the default is
|
|
308
|
+
* right — and today deploy is the only caller, because publish PROMOTES the
|
|
309
|
+
* staged documents rather than re-projecting them. */
|
|
310
|
+
export interface PromotedRuleConfig {
|
|
300
311
|
participantRead?: readonly string[];
|
|
301
|
-
|
|
312
|
+
collections?: Record<string, AuthoredCollectionConfig>;
|
|
313
|
+
}
|
|
314
|
+
/** What this audience may CHANGE, per collection it draws.
|
|
315
|
+
*
|
|
316
|
+
* The `collections` config is the PROMOTED one where there is one: at publish
|
|
317
|
+
* the rules run against what deploy staged, so projecting the manifest's
|
|
318
|
+
* would advertise transitions the live rules deny. */
|
|
319
|
+
export declare function tierWrites(authored: AuthoredApp, audience: Exclude<ViewAudience, "public">, cids: string[], promoted: PromotedRuleConfig): ProjectedViewWrite[];
|
|
320
|
+
/** What this audience may READ, and how to query for it.
|
|
321
|
+
*
|
|
322
|
+
* A collection with no scope is dropped rather than published as unreachable:
|
|
323
|
+
* the gate has already refused the declaration, so reaching here with one is
|
|
324
|
+
* a programming error, and a page that queries it is denied. */
|
|
325
|
+
export declare function tierViews(authored: AuthoredApp, audience: Exclude<ViewAudience, "public">, views: NormalizedView[], participantRead: readonly string[]): {
|
|
326
|
+
id: string;
|
|
327
|
+
collections: ProjectedViewCollection[];
|
|
328
|
+
}[];
|
|
329
|
+
export declare function projectAppViews(authored: AuthoredApp, stamp: PublishStamp, promoted?: PromotedRuleConfig): AppViewTier[];
|
|
302
330
|
/** The document a tier's projection is published at. Beside the views
|
|
303
331
|
* themselves, under one `match` — see `firestore.rules`. */
|
|
304
332
|
export declare const viewConfigDocId: (stage: "live" | "staged") => string;
|
|
@@ -4,7 +4,7 @@ const require_itemId = require("../itemId-Dzrr3vs5.cjs");
|
|
|
4
4
|
const require_promptSafety = require("../promptSafety-CWqYq-NS.cjs");
|
|
5
5
|
require("../collection/index.cjs");
|
|
6
6
|
const require_discovery = require("../discovery-BsXiDZBR.cjs");
|
|
7
|
-
const require_server = require("../server-
|
|
7
|
+
const require_server = require("../server-DLzQ4jFu.cjs");
|
|
8
8
|
const require_notifier = require("../notifier-DZJj0sOh.cjs");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
10
10
|
//#region src/collection-watchers/config.ts
|
|
@@ -3,7 +3,7 @@ import { B as fieldText, x as isValidCollectionName } from "../itemId-DfTm08jm.j
|
|
|
3
3
|
import { E as whenMatches, f as itemIsDone } from "../promptSafety-CSdyp7RF.js";
|
|
4
4
|
import "../collection/index.js";
|
|
5
5
|
import { H as itemFilePath, Q as collectionChangePayload, d as storeFor, lt as publishCollectionChange, n as discoverCollections, pt as sharedCollectionChangePayload, r as loadCollection, st as peekWorkspaceRoot, tt as firestoreHandle } from "../discovery-B9zdNkgW.js";
|
|
6
|
-
import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-
|
|
6
|
+
import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-jm7aGA3g.js";
|
|
7
7
|
import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-DhnwR82U.js";
|
|
8
8
|
import { access } from "node:fs/promises";
|
|
9
9
|
//#region src/collection-watchers/config.ts
|
|
@@ -5,7 +5,7 @@ const require_root = require("../../root-rPH6FGDT.cjs");
|
|
|
5
5
|
const require_itemId = require("../../itemId-Dzrr3vs5.cjs");
|
|
6
6
|
const require_project = require("../../project-C1ep9pvo.cjs");
|
|
7
7
|
const require_discovery = require("../../discovery-BsXiDZBR.cjs");
|
|
8
|
-
const require_server = require("../../server-
|
|
8
|
+
const require_server = require("../../server-DLzQ4jFu.cjs");
|
|
9
9
|
const require_feeds_paths = require("../paths.cjs");
|
|
10
10
|
const require_ingestTypes = require("../../ingestTypes-B4XfrUoq.cjs");
|
|
11
11
|
const require_notifier = require("../../notifier-DZJj0sOh.cjs");
|
|
@@ -3,7 +3,7 @@ import { t as canonicalRoot } from "../../root-BMroU_mB.js";
|
|
|
3
3
|
import { F as INGEST_KINDS, P as FEED_SCHEDULES } from "../../itemId-DfTm08jm.js";
|
|
4
4
|
import { t as mergeIntoExisting } from "../../project-B0j_Vf_Q.js";
|
|
5
5
|
import { K as safeSlugName, U as resolveDataDir, _t as createHostSlot, d as storeFor, gt as createForwardingLogger, k as listItems, n as discoverCollections } from "../../discovery-B9zdNkgW.js";
|
|
6
|
-
import { I as readSkillTemplate, M as buildCollectionActionSeedPrompt, N as promptPathsFor } from "../../server-
|
|
6
|
+
import { I as readSkillTemplate, M as buildCollectionActionSeedPrompt, N as promptPathsFor } from "../../server-jm7aGA3g.js";
|
|
7
7
|
import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
|
|
8
8
|
import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-B312BwXg.js";
|
|
9
9
|
import { d as publish, n as clear } from "../../notifier-DhnwR82U.js";
|
|
@@ -172,6 +172,90 @@ function participantScope(app, cid, participantRead) {
|
|
|
172
172
|
* prefixes, so a single `match` covers the projection and every view. */
|
|
173
173
|
var viewDocId = (stage, viewId) => `${stage}:${viewId}`;
|
|
174
174
|
var VIEW_CONFIG_ID = "config";
|
|
175
|
+
/** The role a member holds on one collection, by the rules' own resolution:
|
|
176
|
+
* the per-collection entry, else the `*` fallback, else none. */
|
|
177
|
+
function roleOn(app, address, cid) {
|
|
178
|
+
const held = app.members[address];
|
|
179
|
+
if (held === void 0) return void 0;
|
|
180
|
+
return held[cid] ?? held["*"];
|
|
181
|
+
}
|
|
182
|
+
/** The addresses holding one of `roles` on `cid`.
|
|
183
|
+
*
|
|
184
|
+
* Sorted, for the same reason `memberEmails` is: a second publish of an
|
|
185
|
+
* unchanged declaration must produce an unchanged document. */
|
|
186
|
+
function holdersOf(app, cid, roles) {
|
|
187
|
+
return Object.keys(app.members).filter((address) => roles.includes(roleOn(app, address, cid) ?? "")).sort();
|
|
188
|
+
}
|
|
189
|
+
/** Who may write every row of `cid`, and who may write only their own.
|
|
190
|
+
*
|
|
191
|
+
* WHY ADDRESSES ARE PUBLISHED AT ALL. One `member/config` document is read by
|
|
192
|
+
* everyone the tier admits, and the tier only establishes that somebody holds
|
|
193
|
+
* SOME role SOMEWHERE — so a `viewer`, or a stylist scoped to another
|
|
194
|
+
* collection, reads the same entry as the front desk. Without these lists the
|
|
195
|
+
* page would draw approve and reassign for all of them and the rules would
|
|
196
|
+
* refuse when pressed, which is the declaration/enforcement mismatch this
|
|
197
|
+
* whole mechanism exists to prevent.
|
|
198
|
+
*
|
|
199
|
+
* It cannot be answered per principal instead: the document is written once
|
|
200
|
+
* at publish and read by many, and the reader cannot look their own role up —
|
|
201
|
+
* `apps/{aid}` is `readerOf(a, '*')`, and a stylist carrying only
|
|
202
|
+
* `{bookings: "editor"}` holds no `*` role. So the ROSTER'S ANSWER travels
|
|
203
|
+
* with the declaration and the page compares its own address to it.
|
|
204
|
+
*
|
|
205
|
+
* The cost is that staff addresses are visible to staff. That is already true
|
|
206
|
+
* of the approval mail they send each other, and participants read the
|
|
207
|
+
* `roster` tier, which never carries these.
|
|
208
|
+
*
|
|
209
|
+
* A SNAPSHOT, like everything else published: a member added since the last
|
|
210
|
+
* publish is absent until the next one. The rules are the authority either
|
|
211
|
+
* way — this only decides which buttons are drawn. */
|
|
212
|
+
function writersOf(app, cid) {
|
|
213
|
+
return holdersOf(app, cid, ["owner", "editor"]);
|
|
214
|
+
}
|
|
215
|
+
/** The transition half: which table applies, and the field it moves.
|
|
216
|
+
*
|
|
217
|
+
* Both halves or neither. A status field with no table would offer every
|
|
218
|
+
* value; a table with no field has nothing to write it to. */
|
|
219
|
+
function transitionPart(app, audience, cid) {
|
|
220
|
+
const config = app.collections?.[cid];
|
|
221
|
+
const transitions = audience === "member" ? config?.transitions : app.public?.submit?.[cid]?.selfTransitions;
|
|
222
|
+
if (config?.statusField === void 0 || transitions === void 0) return {};
|
|
223
|
+
const part = {
|
|
224
|
+
statusField: config.statusField,
|
|
225
|
+
transitions
|
|
226
|
+
};
|
|
227
|
+
if (audience === "member" && config.mail !== void 0) part.mail = config.mail;
|
|
228
|
+
return part;
|
|
229
|
+
}
|
|
230
|
+
/** The assignment half. `member` only — see {@link writersOf}.
|
|
231
|
+
*
|
|
232
|
+
* `rowWriters` rides here rather than beside `writers`, because the
|
|
233
|
+
* `assignee` role grants nothing at all without a field to compare against
|
|
234
|
+
* (`isAssigned` in the rules requires one, and publish refuses the pair). */
|
|
235
|
+
function assignPart(app, audience, cid) {
|
|
236
|
+
const assigneeField = app.collections?.[cid]?.assigneeField;
|
|
237
|
+
if (audience !== "member" || assigneeField === void 0) return {};
|
|
238
|
+
return {
|
|
239
|
+
assigneeField,
|
|
240
|
+
rowWriters: holdersOf(app, cid, ["assignee"])
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** What `audience` may change about `cid`, or null when the answer is nothing.
|
|
244
|
+
*
|
|
245
|
+
* The two audiences differ in WHICH transition table applies, in whether
|
|
246
|
+
* assignment exists at all, and in whether the roster's answer travels with
|
|
247
|
+
* it; they agree that the status field is the collection's, since the rules
|
|
248
|
+
* read one field either way. */
|
|
249
|
+
function writeFor(app, audience, cid) {
|
|
250
|
+
const write = {
|
|
251
|
+
cid,
|
|
252
|
+
...transitionPart(app, audience, cid),
|
|
253
|
+
...assignPart(app, audience, cid)
|
|
254
|
+
};
|
|
255
|
+
if (Object.keys(write).length === 1) return null;
|
|
256
|
+
if (audience === "member") write.writers = writersOf(app, cid);
|
|
257
|
+
return write;
|
|
258
|
+
}
|
|
175
259
|
//#endregion
|
|
176
260
|
//#region src/collection/server/publishManifest.ts
|
|
177
261
|
/** A collection id / app id, held to the one name rule (`SAFE_SLUG_PATTERN`)
|
|
@@ -806,37 +890,59 @@ function scopeFor(authored, audience, cid, participantRead) {
|
|
|
806
890
|
scope: "all"
|
|
807
891
|
} : participantScope(authored, cid, participantRead);
|
|
808
892
|
}
|
|
809
|
-
/**
|
|
893
|
+
/** What this audience may CHANGE, per collection it draws.
|
|
810
894
|
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
895
|
+
* The `collections` config is the PROMOTED one where there is one: at publish
|
|
896
|
+
* the rules run against what deploy staged, so projecting the manifest's
|
|
897
|
+
* would advertise transitions the live rules deny. */
|
|
898
|
+
function tierWrites(authored, audience, cids, promoted) {
|
|
899
|
+
const effective = promoted.collections === void 0 ? authored : {
|
|
900
|
+
...authored,
|
|
901
|
+
collections: promoted.collections
|
|
902
|
+
};
|
|
903
|
+
return cids.map((cid) => writeFor(effective, audience, cid)).filter((entry) => entry !== null);
|
|
904
|
+
}
|
|
905
|
+
/** What this audience may READ, and how to query for it.
|
|
906
|
+
*
|
|
907
|
+
* A collection with no scope is dropped rather than published as unreachable:
|
|
908
|
+
* the gate has already refused the declaration, so reaching here with one is
|
|
909
|
+
* a programming error, and a page that queries it is denied. */
|
|
910
|
+
function tierViews(authored, audience, views, participantRead) {
|
|
911
|
+
return views.map((view) => ({
|
|
912
|
+
id: view.id,
|
|
913
|
+
collections: view.collections.map((cid) => scopeFor(authored, audience, cid, participantRead)).filter((scope) => scope !== null)
|
|
914
|
+
}));
|
|
915
|
+
}
|
|
916
|
+
/** One tier's projection: what this audience may read, and what it may change. */
|
|
917
|
+
function tierConfig(authored, audience, views, stamp, promoted) {
|
|
918
|
+
const cids = [...new Set(views.flatMap((view) => view.collections))];
|
|
919
|
+
const config = {
|
|
920
|
+
write: tierWrites(authored, audience, cids, promoted),
|
|
921
|
+
views: tierViews(authored, audience, views, promoted.participantRead ?? authored.participantRead ?? []),
|
|
922
|
+
submit: tierSubmit(authored, cids),
|
|
923
|
+
publishedAt: stamp.publishedAt
|
|
924
|
+
};
|
|
925
|
+
if (authored.name !== void 0) config.name = authored.name;
|
|
926
|
+
return config;
|
|
927
|
+
}
|
|
928
|
+
/** The submit declarations for the collections these views draw, so a page can
|
|
929
|
+
* show what may be sent rather than discovering it from a denial. */
|
|
930
|
+
function tierSubmit(authored, cids) {
|
|
931
|
+
const declared = authored.public?.submit ?? {};
|
|
932
|
+
return Object.fromEntries(cids.flatMap((cid) => {
|
|
933
|
+
const spec = declared[cid];
|
|
934
|
+
return spec === void 0 ? [] : [[cid, projectSubmit(spec)]];
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
function projectAppViews(authored, stamp, promoted = {}) {
|
|
817
938
|
const normalized = normalizeViews(authored);
|
|
818
939
|
if (!normalized.ok) throw new Error(`publish: views declaration is not publishable (${normalized.problems.join(" ")})`);
|
|
819
940
|
return ["member", "participant"].map((audience) => {
|
|
820
941
|
const views = normalized.views.filter((view) => view.audience === audience);
|
|
821
|
-
const cids = [...new Set(views.flatMap((view) => view.collections))];
|
|
822
|
-
const declaredSubmit = authored.public?.submit ?? {};
|
|
823
|
-
const submit = Object.fromEntries(cids.flatMap((cid) => {
|
|
824
|
-
const spec = declaredSubmit[cid];
|
|
825
|
-
return spec === void 0 ? [] : [[cid, projectSubmit(spec)]];
|
|
826
|
-
}));
|
|
827
|
-
const config = {
|
|
828
|
-
views: views.map((view) => ({
|
|
829
|
-
id: view.id,
|
|
830
|
-
collections: view.collections.map((cid) => scopeFor(authored, audience, cid, participantRead)).filter((scope) => scope !== null)
|
|
831
|
-
})),
|
|
832
|
-
submit,
|
|
833
|
-
publishedAt: stamp.publishedAt
|
|
834
|
-
};
|
|
835
|
-
if (authored.name !== void 0) config.name = authored.name;
|
|
836
942
|
return {
|
|
837
943
|
tier: VIEW_TIER[audience],
|
|
838
944
|
audience,
|
|
839
|
-
config,
|
|
945
|
+
config: tierConfig(authored, audience, views, stamp, promoted),
|
|
840
946
|
views
|
|
841
947
|
};
|
|
842
948
|
});
|
|
@@ -3950,5 +4056,11 @@ Object.defineProperty(exports, "viewDocId", {
|
|
|
3950
4056
|
return viewDocId;
|
|
3951
4057
|
}
|
|
3952
4058
|
});
|
|
4059
|
+
Object.defineProperty(exports, "writeFor", {
|
|
4060
|
+
enumerable: true,
|
|
4061
|
+
get: function() {
|
|
4062
|
+
return writeFor;
|
|
4063
|
+
}
|
|
4064
|
+
});
|
|
3953
4065
|
|
|
3954
|
-
//# sourceMappingURL=server-
|
|
4066
|
+
//# sourceMappingURL=server-DLzQ4jFu.cjs.map
|