@mulmoclaude/core 3.10.0 → 3.12.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.
@@ -5,7 +5,7 @@ const require_root = require("../../../root-rPH6FGDT.cjs");
5
5
  const require_relPath = require("../../../relPath-CTAjGdCL.cjs");
6
6
  const require_utils_fetch = require("../../../utils/fetch.cjs");
7
7
  const require_discovery = require("../../../discovery-BsXiDZBR.cjs");
8
- const require_server = require("../../../server-CgkkWWBd.cjs");
8
+ const require_server = require("../../../server-BIKy55wo.cjs");
9
9
  const require_templatePath = require("../../../templatePath-27vUZowm.cjs");
10
10
  const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
11
11
  const require_types = require("../../../types-CNqkLT4M.cjs");
@@ -3,7 +3,7 @@ import { n as writeFileAtomic } from "../../../root-BMroU_mB.js";
3
3
  import { n as toPosixRelPath } from "../../../relPath-DW8MC8VO.js";
4
4
  import { fetchWithTimeout } from "../../../utils/fetch.js";
5
5
  import { $ as collectionsRegistriesConfigPath, G as safeRecordId, ct as projectSkillsDir, mt as skillsStagingDir, nt as getWorkspaceRoot, ot as log, r as loadCollection, s as CollectionSchemaZ, t as acceptParsedSchema } from "../../../discovery-B9zdNkgW.js";
6
- import { g as ONE_SECOND_MS } from "../../../server-BNe8XMtu.js";
6
+ import { g as ONE_SECOND_MS } from "../../../server-DkT40KgR.js";
7
7
  import { t as isSafeActionTemplatePath } from "../../../templatePath-k_WNbL_Q.js";
8
8
  import { claudeSkillDir, dataSkillDir, mirrorSkillWrite } from "../../../skill-bridge/index.js";
9
9
  import { n as parseRegistryIndex, r as isRecord, t as OFFICIAL_REGISTRY_NAME } from "../../../types-BKVZrtyc.js";
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_root = require("../../root-rPH6FGDT.cjs");
3
3
  const require_itemId = require("../../itemId-Dzrr3vs5.cjs");
4
4
  const require_discovery = require("../../discovery-BsXiDZBR.cjs");
5
- const require_server = require("../../server-CgkkWWBd.cjs");
5
+ const require_server = require("../../server-BIKy55wo.cjs");
6
6
  const require_templatePath = require("../../templatePath-27vUZowm.cjs");
7
7
  exports.APPS_COLLECTION = require_server.APPS_COLLECTION;
8
8
  exports.APP_MANIFEST_FILE = require_discovery.APP_MANIFEST_FILE;
@@ -89,6 +89,7 @@ exports.projectDeploy = require_server.projectDeploy;
89
89
  exports.projectItemFields = require_discovery.projectItemFields;
90
90
  exports.projectPublish = require_server.projectPublish;
91
91
  exports.promoteSchema = require_server.promoteSchema;
92
+ exports.promotedRoleProblems = require_server.promotedRoleProblems;
92
93
  exports.promptPathsFor = require_server.promptPathsFor;
93
94
  exports.publishCollectionChange = require_discovery.publishCollectionChange;
94
95
  exports.publishProblems = require_server.publishProblems;
@@ -4,7 +4,7 @@ export { sharedItemsPath } from './firestoreStore';
4
4
  export { loadAppManifest, parseAppManifest, appManifestReason, APP_MANIFEST_FILE, type AppManifest, type AppManifestResult } from './appManifest';
5
5
  export { AuthoredAppZ, parseAuthoredApp, APP_ROLES, type AuthoredApp, type AuthoredCollectionConfig, type AuthoredMail, type AuthoredSubmit, } from './publishManifest';
6
6
  export { projectApp, projectDeploy, projectPublish, stagedRuleConfig, promoteSchema, appStagingPath, APP_SLUGS_COLLECTION, appSlugDoc, type AppSlugDoc, type DeployedApp, type PublishedFace, APPS_COLLECTION, PUBLIC_CONFIG_DOC, appConfigPath, appSchemasPath, type PublishStamp, type PublishedApp, type PublishedConfigDoc, type PublishedSchemaDoc, type StagedSchemaDoc, } from './publishProject';
7
- export { publishProblems, bindsSubmitterIdentity, type PublishableCollection } from './publishChecks';
7
+ export { publishProblems, promotedRoleProblems, bindsSubmitterIdentity, type PublishableCollection } from './publishChecks';
8
8
  export type { LoadedCollection } from './discoveredCollection';
9
9
  export * from './paths';
10
10
  export * from './templatePath';
@@ -1,6 +1,6 @@
1
1
  import { t as canonicalRoot } from "../../root-BMroU_mB.js";
2
2
  import { N as COMPUTED_TYPES } from "../../itemId-DfTm08jm.js";
3
3
  import { A as readItem, B as SCHEMA_FILE, C as CollectionQueryZ, D as generateItemId, E as deleteItem, F as loadAppManifest, G as safeRecordId, H as itemFilePath, I as parseAppManifest, J as isBackendUnavailable, K as safeSlugName, L as sharedItemsPath, M as writeItem, N as APP_MANIFEST_FILE, O as isRegularFile, P as appManifestReason, Q as collectionChangePayload, R as pageFromFullRead, S as compileJsonlQuery, T as MAX_QUERY_ROWS, U as resolveDataDir, V as isContainedInRoot, W as resolveTemplatePath, Y as COLLECTION_ROOT_REQUIRED, Z as collectionChangeKey, _ as dedupeByRecordId, a as toDetail, at as localCollectionKey, d as storeFor, dt as setFirestoreAccessor, et as configureCollectionHost, ft as setSharedCollectionsSupport, g as decodeCsvRecordId, h as csvRowToItem, i as resolvePrimaryField, j as resolveCreateItemId, k as listItems, l as collectionWritable, lt as publishCollectionChange, n as discoverCollections, nt as getWorkspaceRoot, o as toSummary, ot as log, p as MAX_CSV_ROWS, pt as sharedCollectionChangePayload, q as BackendUnavailableError, r as loadCollection, rt as hostSupportsSharedCollections, s as CollectionSchemaZ, st as peekWorkspaceRoot, t as acceptParsedSchema, tt as firestoreHandle, u as readOnlyRefusal, ut as setCollectionChangePublisher, v as encodeCsvRecordId, w as DEFAULT_QUERY_ROWS, x as compileCsvQuery, y as normalizeCsvValue, z as projectItemFields } from "../../discovery-B9zdNkgW.js";
4
- import { $ as parseAuthoredApp, A as runQueryOverRows, B as APP_SLUGS_COLLECTION, C as STORE_UNREADABLE, D as recordFieldProblem, E as compileRecordZ, F as readCustomViewI18n, G as appStagingPath, H as appConfigPath, I as readSkillTemplate, J as projectPublish, K as projectApp, L as bindsSubmitterIdentity, M as buildCollectionActionSeedPrompt, N as promptPathsFor, O as runCollectionQuery, P as readCustomViewHtml, Q as AuthoredAppZ, R as publishProblems, S as MAX_RECORD_ISSUES, T as validateRecordObject, U as appSchemasPath, V as PUBLIC_CONFIG_DOC, W as appSlugDoc, X as stagedRuleConfig, Y as promoteSchema, Z as APP_ROLES, _ as computeCollectionIcon, a as deleteCollection, b as applyMutateAction, c as computeSuccessor, d as isTriggerDue, f as maybeSpawnSuccessor, h as successorId, i as deleteCustomView, j as buildActionSeedPrompt, k as enrichItems, l as daysInMonth, m as resolveEvery, n as MAX_UNSELECTIVE_ITEMS, o as deleteCollectionRefusalMessage, p as parseCivil, q as projectDeploy, r as makeManageCollectionTool, s as advanceTriggerDate, t as MAX_SCHEMA_ISSUES, u as formatCivil, v as buildWorkspaceOntology, w as validateCollectionRecords, x as firstMutateParamProblem, y as schemaRelations, z as APPS_COLLECTION } from "../../server-BNe8XMtu.js";
4
+ import { $ as AuthoredAppZ, A as runQueryOverRows, B as APPS_COLLECTION, C as STORE_UNREADABLE, D as recordFieldProblem, E as compileRecordZ, F as readCustomViewI18n, G as appSlugDoc, H as PUBLIC_CONFIG_DOC, I as readSkillTemplate, J as projectDeploy, K as appStagingPath, L as bindsSubmitterIdentity, M as buildCollectionActionSeedPrompt, N as promptPathsFor, O as runCollectionQuery, P as readCustomViewHtml, Q as APP_ROLES, R as promotedRoleProblems, S as MAX_RECORD_ISSUES, T as validateRecordObject, U as appConfigPath, V as APP_SLUGS_COLLECTION, W as appSchemasPath, X as promoteSchema, Y as projectPublish, Z as stagedRuleConfig, _ as computeCollectionIcon, a as deleteCollection, b as applyMutateAction, c as computeSuccessor, d as isTriggerDue, et as parseAuthoredApp, f as maybeSpawnSuccessor, h as successorId, i as deleteCustomView, j as buildActionSeedPrompt, k as enrichItems, l as daysInMonth, m as resolveEvery, n as MAX_UNSELECTIVE_ITEMS, o as deleteCollectionRefusalMessage, p as parseCivil, q as projectApp, r as makeManageCollectionTool, s as advanceTriggerDate, t as MAX_SCHEMA_ISSUES, u as formatCivil, v as buildWorkspaceOntology, w as validateCollectionRecords, x as firstMutateParamProblem, y as schemaRelations, z as publishProblems } from "../../server-DkT40KgR.js";
5
5
  import { i as isSafeTemplatePath, n as isSafeCustomViewI18nPath, r as isSafeCustomViewPath, t as isSafeActionTemplatePath } from "../../templatePath-k_WNbL_Q.js";
6
- export { APPS_COLLECTION, APP_MANIFEST_FILE, APP_ROLES, APP_SLUGS_COLLECTION, AuthoredAppZ, BackendUnavailableError, COLLECTION_ROOT_REQUIRED, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_RECORD_ISSUES, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, PUBLIC_CONFIG_DOC, SCHEMA_FILE, STORE_UNREADABLE, acceptParsedSchema, advanceTriggerDate, appConfigPath, appManifestReason, appSchemasPath, appSlugDoc, appStagingPath, applyMutateAction, bindsSubmitterIdentity, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, canonicalRoot, collectionChangeKey, collectionChangePayload, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firestoreHandle, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, hostSupportsSharedCollections, isBackendUnavailable, isContainedInRoot, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadAppManifest, loadCollection, localCollectionKey, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseAppManifest, parseAuthoredApp, parseCivil, peekWorkspaceRoot, projectApp, projectDeploy, projectItemFields, projectPublish, promoteSchema, promptPathsFor, publishCollectionChange, publishProblems, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolvePrimaryField, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, setFirestoreAccessor, setSharedCollectionsSupport, sharedCollectionChangePayload, sharedItemsPath, stagedRuleConfig, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
6
+ export { APPS_COLLECTION, APP_MANIFEST_FILE, APP_ROLES, APP_SLUGS_COLLECTION, AuthoredAppZ, BackendUnavailableError, COLLECTION_ROOT_REQUIRED, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_RECORD_ISSUES, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, PUBLIC_CONFIG_DOC, SCHEMA_FILE, STORE_UNREADABLE, acceptParsedSchema, advanceTriggerDate, appConfigPath, appManifestReason, appSchemasPath, appSlugDoc, appStagingPath, applyMutateAction, bindsSubmitterIdentity, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, canonicalRoot, collectionChangeKey, collectionChangePayload, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firestoreHandle, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, hostSupportsSharedCollections, isBackendUnavailable, isContainedInRoot, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadAppManifest, loadCollection, localCollectionKey, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseAppManifest, parseAuthoredApp, parseCivil, peekWorkspaceRoot, projectApp, projectDeploy, projectItemFields, projectPublish, promoteSchema, promotedRoleProblems, promptPathsFor, publishCollectionChange, publishProblems, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolvePrimaryField, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, setFirestoreAccessor, setSharedCollectionsSupport, sharedCollectionChangePayload, sharedItemsPath, stagedRuleConfig, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -1,4 +1,5 @@
1
1
  import { AuthoredApp, AuthoredSubmit } from './publishManifest';
2
+ import { StagedSchemaDoc } from './publishProject';
2
3
  /** What publish knows about a shared collection in this repository, as far as
3
4
  * these checks are concerned: its cid and the schema key its records are
4
5
  * identified by. The whole schema is deliberately not threaded in — these
@@ -27,3 +28,31 @@ export declare function bindsSubmitterIdentity(submit: AuthoredSubmit): boolean;
27
28
  * All of them, every time. Publish is a manual step with a human waiting on
28
29
  * it; stopping at the first problem turns one review into five. */
29
30
  export declare function publishProblems(app: AuthoredApp, collections: readonly PublishableCollection[], publisherEmail: string): string[];
31
+ /** What publish will actually promote, checked as the PAIR it becomes.
32
+ *
33
+ * `publishProblems` reads the manifest, where `members` and `collections` sit
34
+ * side by side and agree. Publish does not write that pair. It writes the
35
+ * roster from the manifest and the collection configuration from what DEPLOY
36
+ * staged, so the app that lands is one half of each — and no check has ever
37
+ * looked at that combination.
38
+ *
39
+ * The sequence that gets through: deploy revision A with no `assigneeField`,
40
+ * add the field AND the member in revision B, publish without redeploying.
41
+ * Every manifest-level check passes on a declaration that is internally sound,
42
+ * while what lands is A's field-less configuration beside B's roster — an
43
+ * assignee with nothing to be compared against, refused every write, in an app
44
+ * that keeps working for everybody else. That is the precise trap
45
+ * `assigneeProblems` exists to prevent, reached by the one route it cannot
46
+ * see.
47
+ *
48
+ * Separate from `publishProblems` because it needs what deploy staged, which
49
+ * is a Firestore read the host makes and this package does not. It is checked
50
+ * against `stagedRuleConfig` — the same function the projection uses — rather
51
+ * than against a re-derivation, so the value validated is the value written.
52
+ *
53
+ * Only the staged half can be stale, so only that half is named and the fix is
54
+ * "deploy again" rather than "fix the declaration". */
55
+ export declare function promotedRoleProblems(app: AuthoredApp, staged: {
56
+ cid: string;
57
+ doc: StagedSchemaDoc;
58
+ }[]): string[];
@@ -1,7 +1,23 @@
1
1
  import { z } from 'zod';
2
- /** The four roles the deployed rules understand. `participant` is the layer
3
- * that is NAMED but reads only its own rows — see `readerOf` vs `listedIn`. */
4
- export declare const APP_ROLES: readonly ["owner", "editor", "viewer", "participant"];
2
+ /** The roles the deployed rules understand.
3
+ *
4
+ * Two of them are row-scoped, in opposite directions, and the pair is what
5
+ * the four-way split could not express:
6
+ *
7
+ * `participant` — the layer that is NAMED but reads only its OWN rows (the
8
+ * rows it submitted). See `readerOf` vs `listedIn`.
9
+ *
10
+ * `assignee` — reads EVERY row and writes only the rows ASSIGNED to it. The
11
+ * stylist who approves their own bookings and not a colleague's; the marker
12
+ * who grades their own students. Which rows are theirs is
13
+ * `collections[cid].assigneeField`, a field on the record holding the
14
+ * member's address. Reads are deliberately unscoped: a stylist needs the
15
+ * whole day's schedule, and scoping the read makes the app unusable.
16
+ *
17
+ * The names are permanent. The deployed rules compare these strings directly
18
+ * and they are written into `app.json` files people commit, so a rename is a
19
+ * migration over published apps rather than an edit. */
20
+ export declare const APP_ROLES: readonly ["owner", "editor", "viewer", "participant", "assignee"];
5
21
  /** The declarative mail queue, as the rules re-derive it: a transition of the
6
22
  * status field, a recipient read off the RECORD, and a fixed template. */
7
23
  declare const MailZ: z.ZodObject<{
@@ -19,6 +35,7 @@ declare const CollectionConfigZ: z.ZodObject<{
19
35
  transitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
20
36
  immutable: z.ZodOptional<z.ZodBoolean>;
21
37
  submitOnly: z.ZodOptional<z.ZodBoolean>;
38
+ assigneeField: z.ZodOptional<z.ZodString>;
22
39
  peerVisibility: z.ZodOptional<z.ZodEnum<{
23
40
  public: "public";
24
41
  hidden: "hidden";
@@ -63,7 +80,13 @@ declare const SubmitZ: z.ZodObject<{
63
80
  window: z.ZodOptional<z.ZodObject<{
64
81
  from: z.ZodOptional<z.ZodISODateTime>;
65
82
  until: z.ZodOptional<z.ZodISODateTime>;
83
+ fromField: z.ZodOptional<z.ZodObject<{
84
+ ref: z.ZodString;
85
+ collection: z.ZodString;
86
+ field: z.ZodString;
87
+ }, z.core.$strict>>;
66
88
  }, z.core.$strict>>;
89
+ stampField: z.ZodOptional<z.ZodString>;
67
90
  selfUpdate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
68
91
  selfTransitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
69
92
  finalize: z.ZodOptional<z.ZodBoolean>;
@@ -84,6 +107,7 @@ declare const SubmitZ: z.ZodObject<{
84
107
  export declare const AuthoredAppZ: z.ZodObject<{
85
108
  aid: z.ZodString;
86
109
  name: z.ZodOptional<z.ZodString>;
110
+ slug: z.ZodOptional<z.ZodString>;
87
111
  aidEnv: z.ZodOptional<z.ZodString>;
88
112
  owner: z.ZodOptional<z.ZodString>;
89
113
  members: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodString]>, z.ZodEnum<{
@@ -91,12 +115,14 @@ export declare const AuthoredAppZ: z.ZodObject<{
91
115
  editor: "editor";
92
116
  viewer: "viewer";
93
117
  participant: "participant";
118
+ assignee: "assignee";
94
119
  }>>>;
95
120
  collections: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
96
121
  statusField: z.ZodOptional<z.ZodString>;
97
122
  transitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
98
123
  immutable: z.ZodOptional<z.ZodBoolean>;
99
124
  submitOnly: z.ZodOptional<z.ZodBoolean>;
125
+ assigneeField: z.ZodOptional<z.ZodString>;
100
126
  peerVisibility: z.ZodOptional<z.ZodEnum<{
101
127
  public: "public";
102
128
  hidden: "hidden";
@@ -145,7 +171,13 @@ export declare const AuthoredAppZ: z.ZodObject<{
145
171
  window: z.ZodOptional<z.ZodObject<{
146
172
  from: z.ZodOptional<z.ZodISODateTime>;
147
173
  until: z.ZodOptional<z.ZodISODateTime>;
174
+ fromField: z.ZodOptional<z.ZodObject<{
175
+ ref: z.ZodString;
176
+ collection: z.ZodString;
177
+ field: z.ZodString;
178
+ }, z.core.$strict>>;
148
179
  }, z.core.$strict>>;
180
+ stampField: z.ZodOptional<z.ZodString>;
149
181
  selfUpdate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
150
182
  selfTransitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
151
183
  finalize: z.ZodOptional<z.ZodBoolean>;
@@ -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-CgkkWWBd.cjs");
7
+ const require_server = require("../server-BIKy55wo.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-BNe8XMtu.js";
6
+ import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-DkT40KgR.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-CgkkWWBd.cjs");
8
+ const require_server = require("../../server-BIKy55wo.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-BNe8XMtu.js";
6
+ import { I as readSkillTemplate, M as buildCollectionActionSeedPrompt, N as promptPathsFor } from "../../server-DkT40KgR.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";
@@ -21,13 +21,30 @@ var NameZ = zod.z.string().refine(require_itemId.isValidCollectionName, { messag
21
21
  * the rules compare it to `request.auth.token.email` verbatim, so any
22
22
  * narrowing here would refuse addresses Firebase itself accepts. */
23
23
  var EmailZ = zod.z.string().trim().min(3).includes("@");
24
- /** The four roles the deployed rules understand. `participant` is the layer
25
- * that is NAMED but reads only its own rows — see `readerOf` vs `listedIn`. */
24
+ /** The roles the deployed rules understand.
25
+ *
26
+ * Two of them are row-scoped, in opposite directions, and the pair is what
27
+ * the four-way split could not express:
28
+ *
29
+ * `participant` — the layer that is NAMED but reads only its OWN rows (the
30
+ * rows it submitted). See `readerOf` vs `listedIn`.
31
+ *
32
+ * `assignee` — reads EVERY row and writes only the rows ASSIGNED to it. The
33
+ * stylist who approves their own bookings and not a colleague's; the marker
34
+ * who grades their own students. Which rows are theirs is
35
+ * `collections[cid].assigneeField`, a field on the record holding the
36
+ * member's address. Reads are deliberately unscoped: a stylist needs the
37
+ * whole day's schedule, and scoping the read makes the app unusable.
38
+ *
39
+ * The names are permanent. The deployed rules compare these strings directly
40
+ * and they are written into `app.json` files people commit, so a rename is a
41
+ * migration over published apps rather than an edit. */
26
42
  var APP_ROLES = [
27
43
  "owner",
28
44
  "editor",
29
45
  "viewer",
30
- "participant"
46
+ "participant",
47
+ "assignee"
31
48
  ];
32
49
  var RoleZ = zod.z.enum(APP_ROLES);
33
50
  /** `{ email: { "*" | cid: role } }`. The `"*"` key is the app-wide role; a
@@ -53,6 +70,21 @@ var CollectionConfigZ = zod.z.object({
53
70
  transitions: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
54
71
  immutable: zod.z.boolean().optional(),
55
72
  submitOnly: zod.z.boolean().optional(),
73
+ /** The field naming the member a row belongs to, for the `assignee` role.
74
+ *
75
+ * Holds an ADDRESS, because that is the only thing the rules can compare
76
+ * a member against (`request.auth.token.email`). A `ref` to a staff
77
+ * collection stores the target's primary-key slug, not an address, so it
78
+ * cannot be this field — declare a plain field beside the ref and let the
79
+ * ref stay the thing the UI renders. The alternative, having the rules
80
+ * `get()` the staff record to read an address off it, costs a document
81
+ * access on every write and puts a second document between an
82
+ * authorization decision and its answer.
83
+ *
84
+ * Only meaningful with a member holding `assignee` on this cid; a
85
+ * declaration with the role and no field is refused (`assigneeProblems`),
86
+ * because that member would silently hold nothing. */
87
+ assigneeField: zod.z.string().trim().min(1).optional(),
56
88
  peerVisibility: zod.z.enum(["public", "hidden"]).optional(),
57
89
  revealGated: zod.z.boolean().optional(),
58
90
  gatedFrom: NameZ.optional(),
@@ -71,9 +103,32 @@ var CollectionConfigZ = zod.z.object({
71
103
  * the rules do not coerce strings, so an ISO string reaching Firestore is a
72
104
  * type error that fails CLOSED (`inWindow` refuses every submission and the
73
105
  * author sees "nobody can submit", not an error). */
106
+ /** A window bound that lives on ANOTHER record, read at write time.
107
+ *
108
+ * `window.from` is one absolute instant for the whole collection, which is
109
+ * enough for a survey and useless for anything recurring: "each class opens
110
+ * three days before it starts, at 08:00" is a bound PER RECORD. So the bound
111
+ * is not computed in the rules — they have no usable date arithmetic and
112
+ * `request.time` is UTC, which is the wrong answer for "08:00" — it is
113
+ * computed by whoever schedules the class, stored on the class record as
114
+ * epoch millis, and merely COMPARED here.
115
+ *
116
+ * `ref` is the field on the record being written that names the target
117
+ * (`classId`); `collection` is the cid the target lives in, fixed in the
118
+ * declaration so that a path is never built out of a value a submitter wrote;
119
+ * `field` is the epoch-millis field on the target.
120
+ *
121
+ * Not spelled `in` — that is an operator in the rules language, and
122
+ * `w.fromField.in` does not parse there. */
123
+ var WindowRefZ = zod.z.object({
124
+ ref: zod.z.string().trim().min(1),
125
+ collection: NameZ,
126
+ field: zod.z.string().trim().min(1)
127
+ }).strict();
74
128
  var WindowZ = zod.z.object({
75
129
  from: zod.z.iso.datetime().optional(),
76
- until: zod.z.iso.datetime().optional()
130
+ until: zod.z.iso.datetime().optional(),
131
+ fromField: WindowRefZ.optional()
77
132
  }).strict();
78
133
  var ValidateZ = zod.z.object({
79
134
  required: zod.z.array(zod.z.string().trim().min(1)).optional(),
@@ -106,6 +161,19 @@ var SubmitZ = zod.z.object({
106
161
  idField: zod.z.string().trim().min(1).optional(),
107
162
  validate: ValidateZ.optional(),
108
163
  window: WindowZ.optional(),
164
+ /** A field the rules PIN to the server clock on create: the record must
165
+ * carry `request.time` in it, and may never change it afterwards.
166
+ *
167
+ * What it buys is an order nobody can jump. A first-come app takes its
168
+ * capacity from rank rather than from a count — the rules cannot count
169
+ * documents, so "the first 8" can only ever be a reading of the rows —
170
+ * and a rank is only as honest as the timestamp it sorts by. `idFrom`
171
+ * stops a person holding two places; nothing else stops them writing
172
+ * yesterday's date into the field that decides who got there first.
173
+ *
174
+ * Binds EVERY create, the writer branch included, so a staff-entered row
175
+ * cannot be back-dated into the queue either. */
176
+ stampField: zod.z.string().trim().min(1).optional(),
109
177
  /** Per CURRENT STATUS, never a flat list: a flat list lets a customer move
110
178
  * an approved booking's `startAt` without anyone re-approving it. */
111
179
  selfUpdate: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
@@ -124,6 +192,34 @@ var PublicZ = zod.z.object({
124
192
  read: zod.z.array(NameZ).optional(),
125
193
  submit: zod.z.record(NameZ, SubmitZ).optional()
126
194
  }).strict();
195
+ /** The URL name an app is handed out under: `https://<host>/{slug}`.
196
+ *
197
+ * A SEPARATE name from the `aid`, and that separation is the point (design
198
+ * D2b). `apps/{aid}` is a shelf every user of the deployment shares and the
199
+ * rules' `allow create` asks only that you name yourself owner — so a
200
+ * memorable aid is first-come-first-served, cannot be checked for
201
+ * availability, and frees up again when an app is deleted. The aid is
202
+ * therefore a UUID, and the thing people can fight over is moved to the name
203
+ * that costs nothing to change.
204
+ *
205
+ * Declared here rather than kept beside `app.json` because a RESERVATION has
206
+ * to travel with the repository: `appSlugs/{slug}` is unreadable until the app
207
+ * is published (`allow read: if resource.data.published == true`), so nothing
208
+ * can recover which slug an app holds by asking Firestore. A second file to
209
+ * keep in step with the declaration is the alternative, and it is the kind of
210
+ * pair that goes out of step silently.
211
+ *
212
+ * The shape is stricter than `NameZ` on purpose: it is BOTH a URL path
213
+ * segment people read aloud and a Firestore document id. Lowercase
214
+ * alphanumerics separated by single hyphens covers both without a case rule
215
+ * that would make two slugs collide in one place and not the other.
216
+ *
217
+ * Which slug an app ended up with is the HOST's business — a wanted slug can
218
+ * be taken, and the host writes the one it reserved back here. Nothing in this
219
+ * package reads the key; it is declared so that writing it back does not make
220
+ * the file unparseable. */
221
+ var SLUG_SHAPE = "must be lowercase letters, digits and single hyphens, and must not start or end with one (e.g. sakura-hair)";
222
+ var SlugZ = zod.z.string().trim().max(64).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/, SLUG_SHAPE).refine((slug) => !slug.includes("--"), SLUG_SHAPE);
127
223
  /** The whole authored declaration.
128
224
  *
129
225
  * `owner` is accepted but is NOT the published value — publish stamps the
@@ -135,6 +231,8 @@ var PublicZ = zod.z.object({
135
231
  var AuthoredAppZ = zod.z.object({
136
232
  aid: NameZ,
137
233
  name: zod.z.string().trim().min(1).optional(),
234
+ /** The wanted (or reserved) URL name — see {@link SlugZ}. */
235
+ slug: SlugZ.optional(),
138
236
  /** Per-worktree app id (design D6, implementation order 7). Accepted so a
139
237
  * repository already carrying it parses; nothing reads it yet. */
140
238
  aidEnv: zod.z.string().trim().min(1).optional(),
@@ -196,7 +294,11 @@ function windowMillis(window) {
196
294
  if (window.from !== void 0) out.fromMs = Date.parse(window.from);
197
295
  if (window.until !== void 0) out.untilMs = Date.parse(window.until);
198
296
  if (Object.values(out).some((value) => !Number.isFinite(value))) throw new Error(`publish: window bound is not a parseable timestamp (${JSON.stringify(window)})`);
199
- return Object.keys(out).length > 0 ? out : void 0;
297
+ const projected = {
298
+ ...out,
299
+ ...window.fromField === void 0 ? {} : { fromField: window.fromField }
300
+ };
301
+ return Object.keys(projected).length > 0 ? projected : void 0;
200
302
  }
201
303
  /** One `public.submit[cid]`, with its window lowered. Everything else passes
202
304
  * through: the rules read these keys by the names the author wrote. */
@@ -623,7 +725,8 @@ function unknownCidProblems(app, collections) {
623
725
  ["collections", Object.keys(app.collections ?? {})],
624
726
  ["public.read", app.public?.read ?? []],
625
727
  ["public.submit", Object.keys(app.public?.submit ?? {})],
626
- ["participantRead", app.participantRead ?? []]
728
+ ["participantRead", app.participantRead ?? []],
729
+ ["members", [...new Set(Object.values(app.members).flatMap((roles) => Object.keys(roles)))].filter((key) => key !== "*")]
627
730
  ].flatMap(([where, cids]) => cids.filter((cid) => !known.has(cid)).map((cid) => `${where} names '${cid}', which is not a shared collection in this repository. Shared collections here: ${known.size > 0 ? [...known].sort().join(", ") : "(none - a schema needs storage.type \"firestore\")"}.`));
628
731
  }
629
732
  /** Everything publish refuses, as lines the author can act on.
@@ -640,7 +743,10 @@ function publishProblems(app, collections, publisherEmail) {
640
743
  ...mailProblems(app),
641
744
  ...submitShapeProblems(app),
642
745
  ...coherenceProblems(app),
643
- ...primaryKeyProblems(app, collections)
746
+ ...primaryKeyProblems(app, collections),
747
+ ...assigneeProblems(app),
748
+ ...stampProblems(app),
749
+ ...windowRefProblems(app, collections)
644
750
  ];
645
751
  }
646
752
  /** A public submission must NOT be allowed to name its own primary key.
@@ -670,6 +776,99 @@ function primaryKeyProblems(app, collections) {
670
776
  return [`public.submit.${cid}.createFields must NOT include "${primaryKey}", the schema's primaryKey: the rules can pin the document id but not the value of a field, so a submitter could write at their own id while claiming another record's. A shared record's identity is its document id — the store fills the field from it, and a submitted value is either the same thing or a lie that is thrown away.`];
671
777
  });
672
778
  }
779
+ /** `assignee` without the field that says which rows are theirs.
780
+ *
781
+ * A FAIL-CLOSED trap of the worst kind, because it fails closed for one
782
+ * person and nobody else: the rules ask `collections[cid].assigneeField` for
783
+ * the field to compare, find nothing, and refuse every write that member
784
+ * makes. The app works for the owner who set it up, and the member it was set
785
+ * up for is told only "permission denied".
786
+ *
787
+ * `'*': "assignee"` is refused outright rather than checked against every
788
+ * collection. The role means "the rows assigned to you", and what counts as
789
+ * assigned is per collection — an app-wide one would need the same field name
790
+ * to be right everywhere, and where it is missing it silently means "no
791
+ * access to this collection" rather than "no scoping here".
792
+ */
793
+ function assigneeProblems(app) {
794
+ return Object.entries(app.members).flatMap(([email, roles]) => Object.entries(roles).flatMap(([cid, role]) => {
795
+ if (role !== "assignee") return [];
796
+ if (cid === "*") return [`members["${email}"] holds "assignee" under "*", and the role cannot be app-wide: which rows are yours is declared per collection (\`collections.<cid>.assigneeField\`). Name the collections instead — { "bookings": "assignee" }.`];
797
+ if (app.collections?.[cid]?.assigneeField !== void 0) return [];
798
+ return [`members["${email}"] holds "assignee" on '${cid}', but collections.${cid}.assigneeField does not say which field names the member a row belongs to. Add it (assigneeField: "<a field holding an address>"), or give a role that is not row-scoped. Without it the rules have nothing to compare and refuse every write that member makes, while the app keeps working for everybody else.`];
799
+ }));
800
+ }
801
+ /** A server-stamped field the submitter cannot write, or can rewrite later.
802
+ *
803
+ * Both failures are silent in opposite directions. Left out of
804
+ * `createFields`, the rules refuse every submission (`hasOnly(createFields)`
805
+ * rejects the key the stamp check requires) — an app nobody can use. Left IN
806
+ * a `selfUpdate` list, the field the queue is ordered by becomes editable by
807
+ * the person standing in the queue. */
808
+ function stampProblems(app) {
809
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
810
+ const stamp = submit.stampField;
811
+ if (stamp === void 0) return [];
812
+ const problems = [];
813
+ if (!submit.createFields.includes(stamp)) problems.push(`public.submit.${cid}.stampField names '${stamp}', which is not in createFields. The rules require the record to CARRY the server time in that field, and refuse any key outside createFields — so every submission is denied. Add it to createFields; the page fills it in, not the person.`);
814
+ for (const [status, fields] of Object.entries(submit.selfUpdate ?? {})) {
815
+ if (!fields.includes(stamp)) continue;
816
+ problems.push(`public.submit.${cid}.selfUpdate.${status} lets the submitter write '${stamp}', which is the field stampField pins to the server clock. Whatever that field orders — a first-come queue, an audit trail — could then be rewritten by the person it ranks. Remove it from selfUpdate.`);
817
+ }
818
+ return problems;
819
+ });
820
+ }
821
+ /** A per-record window bound pointing at a collection or a field the submitter
822
+ * never writes.
823
+ *
824
+ * `fromField` makes the rules read another record, and every part of that
825
+ * read is fail-closed: an unknown collection, or a `ref` the submission does
826
+ * not carry, means the bound can never be satisfied and the form is shut for
827
+ * good. */
828
+ function windowRefProblems(app, collections) {
829
+ const known = new Set(collections.map((collection) => collection.cid));
830
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
831
+ const ref = submit.window?.fromField;
832
+ if (ref === void 0) return [];
833
+ const problems = [];
834
+ if (!known.has(ref.collection)) problems.push(`public.submit.${cid}.window.fromField.collection names '${ref.collection}', which is not a shared collection in this repository. The rules read the opening time off a record there, so nothing can ever be submitted. Shared collections here: ${known.size > 0 ? [...known].sort().join(", ") : "(none)"}.`);
835
+ if (!submit.createFields.includes(ref.ref)) problems.push(`public.submit.${cid}.window.fromField.ref names '${ref.ref}', which is not in createFields. The rules take the target record's id from that field ON THE SUBMISSION — if the submitter never writes it, there is nothing to look up and every submission is refused.`);
836
+ return problems;
837
+ });
838
+ }
839
+ /** What publish will actually promote, checked as the PAIR it becomes.
840
+ *
841
+ * `publishProblems` reads the manifest, where `members` and `collections` sit
842
+ * side by side and agree. Publish does not write that pair. It writes the
843
+ * roster from the manifest and the collection configuration from what DEPLOY
844
+ * staged, so the app that lands is one half of each — and no check has ever
845
+ * looked at that combination.
846
+ *
847
+ * The sequence that gets through: deploy revision A with no `assigneeField`,
848
+ * add the field AND the member in revision B, publish without redeploying.
849
+ * Every manifest-level check passes on a declaration that is internally sound,
850
+ * while what lands is A's field-less configuration beside B's roster — an
851
+ * assignee with nothing to be compared against, refused every write, in an app
852
+ * that keeps working for everybody else. That is the precise trap
853
+ * `assigneeProblems` exists to prevent, reached by the one route it cannot
854
+ * see.
855
+ *
856
+ * Separate from `publishProblems` because it needs what deploy staged, which
857
+ * is a Firestore read the host makes and this package does not. It is checked
858
+ * against `stagedRuleConfig` — the same function the projection uses — rather
859
+ * than against a re-derivation, so the value validated is the value written.
860
+ *
861
+ * Only the staged half can be stale, so only that half is named and the fix is
862
+ * "deploy again" rather than "fix the declaration". */
863
+ function promotedRoleProblems(app, staged) {
864
+ const promoted = stagedRuleConfig(staged).collections ?? {};
865
+ const stagedCids = new Set(staged.map((entry) => entry.cid));
866
+ return Object.entries(app.members).flatMap(([email, roles]) => Object.entries(roles).flatMap(([cid, role]) => {
867
+ if (role !== "assignee" || cid === "*" || !stagedCids.has(cid)) return [];
868
+ if (promoted[cid]?.assigneeField !== void 0) return [];
869
+ return [`members["${email}"] holds "assignee" on '${cid}', and the STAGED version of '${cid}' — the one publish promotes — carries no assigneeField, even if app.json declares one now. Publish writes the roster from app.json and the collection configuration from the deploy, so that member would land with nothing to be compared against: refused every write, while the app keeps working for everybody else. Run deploy again, so the version being published is the one the declaration describes.`];
870
+ }));
871
+ }
673
872
  //#endregion
674
873
  //#region src/collection/server/skillAssets.ts
675
874
  /** Read a collection's custom-view HTML, path-safely. `viewFile` is a
@@ -2974,6 +3173,12 @@ Object.defineProperty(exports, "promoteSchema", {
2974
3173
  return promoteSchema;
2975
3174
  }
2976
3175
  });
3176
+ Object.defineProperty(exports, "promotedRoleProblems", {
3177
+ enumerable: true,
3178
+ get: function() {
3179
+ return promotedRoleProblems;
3180
+ }
3181
+ });
2977
3182
  Object.defineProperty(exports, "promptPathsFor", {
2978
3183
  enumerable: true,
2979
3184
  get: function() {
@@ -3059,4 +3264,4 @@ Object.defineProperty(exports, "validateRecordObject", {
3059
3264
  }
3060
3265
  });
3061
3266
 
3062
- //# sourceMappingURL=server-CgkkWWBd.cjs.map
3267
+ //# sourceMappingURL=server-BIKy55wo.cjs.map