@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.
@@ -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
- export declare function projectAppViews(authored: AuthoredApp, stamp: PublishStamp, promoted?: {
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
- }): AppViewTier[];
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-DknYbXBo.cjs");
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-CQSF5fHe.js";
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-DknYbXBo.cjs");
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-CQSF5fHe.js";
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
- /** Project the declaration into the per-audience documents.
893
+ /** What this audience may CHANGE, per collection it draws.
810
894
  *
811
- * Pure, like `projectApp`: the HTML is not here (the host reads the files),
812
- * and neither is the clock. What is here is the answer to "what may this
813
- * audience read, and how" — computed once, so the page never has to guess and
814
- * never has to discover it from a denial. */
815
- function projectAppViews(authored, stamp, promoted) {
816
- const participantRead = promoted?.participantRead ?? authored.participantRead ?? [];
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-DknYbXBo.cjs.map
4066
+ //# sourceMappingURL=server-DLzQ4jFu.cjs.map