@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.
@@ -170,6 +170,90 @@ function participantScope(app, cid, participantRead) {
170
170
  * prefixes, so a single `match` covers the projection and every view. */
171
171
  var viewDocId = (stage, viewId) => `${stage}:${viewId}`;
172
172
  var VIEW_CONFIG_ID = "config";
173
+ /** The role a member holds on one collection, by the rules' own resolution:
174
+ * the per-collection entry, else the `*` fallback, else none. */
175
+ function roleOn(app, address, cid) {
176
+ const held = app.members[address];
177
+ if (held === void 0) return void 0;
178
+ return held[cid] ?? held["*"];
179
+ }
180
+ /** The addresses holding one of `roles` on `cid`.
181
+ *
182
+ * Sorted, for the same reason `memberEmails` is: a second publish of an
183
+ * unchanged declaration must produce an unchanged document. */
184
+ function holdersOf(app, cid, roles) {
185
+ return Object.keys(app.members).filter((address) => roles.includes(roleOn(app, address, cid) ?? "")).sort();
186
+ }
187
+ /** Who may write every row of `cid`, and who may write only their own.
188
+ *
189
+ * WHY ADDRESSES ARE PUBLISHED AT ALL. One `member/config` document is read by
190
+ * everyone the tier admits, and the tier only establishes that somebody holds
191
+ * SOME role SOMEWHERE — so a `viewer`, or a stylist scoped to another
192
+ * collection, reads the same entry as the front desk. Without these lists the
193
+ * page would draw approve and reassign for all of them and the rules would
194
+ * refuse when pressed, which is the declaration/enforcement mismatch this
195
+ * whole mechanism exists to prevent.
196
+ *
197
+ * It cannot be answered per principal instead: the document is written once
198
+ * at publish and read by many, and the reader cannot look their own role up —
199
+ * `apps/{aid}` is `readerOf(a, '*')`, and a stylist carrying only
200
+ * `{bookings: "editor"}` holds no `*` role. So the ROSTER'S ANSWER travels
201
+ * with the declaration and the page compares its own address to it.
202
+ *
203
+ * The cost is that staff addresses are visible to staff. That is already true
204
+ * of the approval mail they send each other, and participants read the
205
+ * `roster` tier, which never carries these.
206
+ *
207
+ * A SNAPSHOT, like everything else published: a member added since the last
208
+ * publish is absent until the next one. The rules are the authority either
209
+ * way — this only decides which buttons are drawn. */
210
+ function writersOf(app, cid) {
211
+ return holdersOf(app, cid, ["owner", "editor"]);
212
+ }
213
+ /** The transition half: which table applies, and the field it moves.
214
+ *
215
+ * Both halves or neither. A status field with no table would offer every
216
+ * value; a table with no field has nothing to write it to. */
217
+ function transitionPart(app, audience, cid) {
218
+ const config = app.collections?.[cid];
219
+ const transitions = audience === "member" ? config?.transitions : app.public?.submit?.[cid]?.selfTransitions;
220
+ if (config?.statusField === void 0 || transitions === void 0) return {};
221
+ const part = {
222
+ statusField: config.statusField,
223
+ transitions
224
+ };
225
+ if (audience === "member" && config.mail !== void 0) part.mail = config.mail;
226
+ return part;
227
+ }
228
+ /** The assignment half. `member` only — see {@link writersOf}.
229
+ *
230
+ * `rowWriters` rides here rather than beside `writers`, because the
231
+ * `assignee` role grants nothing at all without a field to compare against
232
+ * (`isAssigned` in the rules requires one, and publish refuses the pair). */
233
+ function assignPart(app, audience, cid) {
234
+ const assigneeField = app.collections?.[cid]?.assigneeField;
235
+ if (audience !== "member" || assigneeField === void 0) return {};
236
+ return {
237
+ assigneeField,
238
+ rowWriters: holdersOf(app, cid, ["assignee"])
239
+ };
240
+ }
241
+ /** What `audience` may change about `cid`, or null when the answer is nothing.
242
+ *
243
+ * The two audiences differ in WHICH transition table applies, in whether
244
+ * assignment exists at all, and in whether the roster's answer travels with
245
+ * it; they agree that the status field is the collection's, since the rules
246
+ * read one field either way. */
247
+ function writeFor(app, audience, cid) {
248
+ const write = {
249
+ cid,
250
+ ...transitionPart(app, audience, cid),
251
+ ...assignPart(app, audience, cid)
252
+ };
253
+ if (Object.keys(write).length === 1) return null;
254
+ if (audience === "member") write.writers = writersOf(app, cid);
255
+ return write;
256
+ }
173
257
  //#endregion
174
258
  //#region src/collection/server/publishManifest.ts
175
259
  /** A collection id / app id, held to the one name rule (`SAFE_SLUG_PATTERN`)
@@ -804,37 +888,59 @@ function scopeFor(authored, audience, cid, participantRead) {
804
888
  scope: "all"
805
889
  } : participantScope(authored, cid, participantRead);
806
890
  }
807
- /** Project the declaration into the per-audience documents.
891
+ /** What this audience may CHANGE, per collection it draws.
808
892
  *
809
- * Pure, like `projectApp`: the HTML is not here (the host reads the files),
810
- * and neither is the clock. What is here is the answer to "what may this
811
- * audience read, and how" — computed once, so the page never has to guess and
812
- * never has to discover it from a denial. */
813
- function projectAppViews(authored, stamp, promoted) {
814
- const participantRead = promoted?.participantRead ?? authored.participantRead ?? [];
893
+ * The `collections` config is the PROMOTED one where there is one: at publish
894
+ * the rules run against what deploy staged, so projecting the manifest's
895
+ * would advertise transitions the live rules deny. */
896
+ function tierWrites(authored, audience, cids, promoted) {
897
+ const effective = promoted.collections === void 0 ? authored : {
898
+ ...authored,
899
+ collections: promoted.collections
900
+ };
901
+ return cids.map((cid) => writeFor(effective, audience, cid)).filter((entry) => entry !== null);
902
+ }
903
+ /** What this audience may READ, and how to query for it.
904
+ *
905
+ * A collection with no scope is dropped rather than published as unreachable:
906
+ * the gate has already refused the declaration, so reaching here with one is
907
+ * a programming error, and a page that queries it is denied. */
908
+ function tierViews(authored, audience, views, participantRead) {
909
+ return views.map((view) => ({
910
+ id: view.id,
911
+ collections: view.collections.map((cid) => scopeFor(authored, audience, cid, participantRead)).filter((scope) => scope !== null)
912
+ }));
913
+ }
914
+ /** One tier's projection: what this audience may read, and what it may change. */
915
+ function tierConfig(authored, audience, views, stamp, promoted) {
916
+ const cids = [...new Set(views.flatMap((view) => view.collections))];
917
+ const config = {
918
+ write: tierWrites(authored, audience, cids, promoted),
919
+ views: tierViews(authored, audience, views, promoted.participantRead ?? authored.participantRead ?? []),
920
+ submit: tierSubmit(authored, cids),
921
+ publishedAt: stamp.publishedAt
922
+ };
923
+ if (authored.name !== void 0) config.name = authored.name;
924
+ return config;
925
+ }
926
+ /** The submit declarations for the collections these views draw, so a page can
927
+ * show what may be sent rather than discovering it from a denial. */
928
+ function tierSubmit(authored, cids) {
929
+ const declared = authored.public?.submit ?? {};
930
+ return Object.fromEntries(cids.flatMap((cid) => {
931
+ const spec = declared[cid];
932
+ return spec === void 0 ? [] : [[cid, projectSubmit(spec)]];
933
+ }));
934
+ }
935
+ function projectAppViews(authored, stamp, promoted = {}) {
815
936
  const normalized = normalizeViews(authored);
816
937
  if (!normalized.ok) throw new Error(`publish: views declaration is not publishable (${normalized.problems.join(" ")})`);
817
938
  return ["member", "participant"].map((audience) => {
818
939
  const views = normalized.views.filter((view) => view.audience === audience);
819
- const cids = [...new Set(views.flatMap((view) => view.collections))];
820
- const declaredSubmit = authored.public?.submit ?? {};
821
- const submit = Object.fromEntries(cids.flatMap((cid) => {
822
- const spec = declaredSubmit[cid];
823
- return spec === void 0 ? [] : [[cid, projectSubmit(spec)]];
824
- }));
825
- const config = {
826
- views: views.map((view) => ({
827
- id: view.id,
828
- collections: view.collections.map((cid) => scopeFor(authored, audience, cid, participantRead)).filter((scope) => scope !== null)
829
- })),
830
- submit,
831
- publishedAt: stamp.publishedAt
832
- };
833
- if (authored.name !== void 0) config.name = authored.name;
834
940
  return {
835
941
  tier: VIEW_TIER[audience],
836
942
  audience,
837
- config,
943
+ config: tierConfig(authored, audience, views, stamp, promoted),
838
944
  views
839
945
  };
840
946
  });
@@ -3552,6 +3658,6 @@ function makeManageCollectionTool(deps = {}) {
3552
3658
  };
3553
3659
  }
3554
3660
  //#endregion
3555
- export { stagedRuleConfig as $, runQueryOverRows as A, APPS_COLLECTION as B, STORE_UNREADABLE as C, recordFieldProblem as D, compileRecordZ as E, readCustomViewI18n as F, appSlugDoc as G, PUBLIC_CONFIG_DOC as H, readSkillTemplate as I, projectApp as J, appStagingPath as K, bindsSubmitterIdentity as L, buildCollectionActionSeedPrompt as M, promptPathsFor as N, runCollectionQuery as O, readCustomViewHtml as P, promoteSchema as Q, promotedRoleProblems as R, MAX_RECORD_ISSUES as S, validateRecordObject as T, appConfigPath as U, APP_SLUGS_COLLECTION as V, appSchemasPath as W, projectDeploy as X, projectAppViews as Y, projectPublish as Z, computeCollectionIcon as _, deleteCollection as a, RESERVED_VIEW_IDS as at, applyMutateAction as b, computeSuccessor as c, VIEW_ID_PATTERN as ct, isTriggerDue as d, participantScope as dt, viewConfigDocId as et, maybeSpawnSuccessor as f, viewDocId as ft, ONE_SECOND_MS as g, successorId as h, deleteCustomView as i, PUBLIC_VIEW_ID as it, buildActionSeedPrompt as j, enrichItems as k, daysInMonth as l, VIEW_TIER as lt, resolveEvery as m, MAX_UNSELECTIVE_ITEMS as n, AuthoredAppZ as nt, deleteCollectionRefusalMessage as o, VIEW_AUDIENCES as ot, parseCivil as p, appViewTierPath as q, makeManageCollectionTool as r, parseAuthoredApp as rt, advanceTriggerDate as s, VIEW_CONFIG_ID as st, MAX_SCHEMA_ISSUES as t, APP_ROLES as tt, formatCivil as u, normalizeViews as ut, buildWorkspaceOntology as v, validateCollectionRecords as w, firstMutateParamProblem as x, schemaRelations as y, publishProblems as z };
3661
+ export { stagedRuleConfig as $, runQueryOverRows as A, APPS_COLLECTION as B, STORE_UNREADABLE as C, recordFieldProblem as D, compileRecordZ as E, readCustomViewI18n as F, appSlugDoc as G, PUBLIC_CONFIG_DOC as H, readSkillTemplate as I, projectApp as J, appStagingPath as K, bindsSubmitterIdentity as L, buildCollectionActionSeedPrompt as M, promptPathsFor as N, runCollectionQuery as O, readCustomViewHtml as P, promoteSchema as Q, promotedRoleProblems as R, MAX_RECORD_ISSUES as S, validateRecordObject as T, appConfigPath as U, APP_SLUGS_COLLECTION as V, appSchemasPath as W, projectDeploy as X, projectAppViews as Y, projectPublish as Z, computeCollectionIcon as _, deleteCollection as a, RESERVED_VIEW_IDS as at, applyMutateAction as b, computeSuccessor as c, VIEW_ID_PATTERN as ct, isTriggerDue as d, participantScope as dt, viewConfigDocId as et, maybeSpawnSuccessor as f, viewDocId as ft, ONE_SECOND_MS as g, successorId as h, deleteCustomView as i, PUBLIC_VIEW_ID as it, buildActionSeedPrompt as j, enrichItems as k, daysInMonth as l, VIEW_TIER as lt, resolveEvery as m, MAX_UNSELECTIVE_ITEMS as n, AuthoredAppZ as nt, deleteCollectionRefusalMessage as o, VIEW_AUDIENCES as ot, parseCivil as p, writeFor as pt, appViewTierPath as q, makeManageCollectionTool as r, parseAuthoredApp as rt, advanceTriggerDate as s, VIEW_CONFIG_ID as st, MAX_SCHEMA_ISSUES as t, APP_ROLES as tt, formatCivil as u, normalizeViews as ut, buildWorkspaceOntology as v, validateCollectionRecords as w, firstMutateParamProblem as x, schemaRelations as y, publishProblems as z };
3556
3662
 
3557
- //# sourceMappingURL=server-CQSF5fHe.js.map
3663
+ //# sourceMappingURL=server-jm7aGA3g.js.map