@mulmoclaude/core 3.12.0 → 3.13.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.
@@ -4,6 +4,7 @@ import { n as toPosixRelPath } from "./relPath-DW8MC8VO.js";
4
4
  import { B as fieldText, I as embedTargetId, L as isFieldDrivenEvery, N as COMPUTED_TYPES, V as fieldTextOrNull, d as parseIsoDate, f as parseIsoDateTime, x as isValidCollectionName, z as storageKindFor } from "./itemId-DfTm08jm.js";
5
5
  import { C as actionVisible, _ as uniqueRefTargets, b as projectBacklinkRow, c as selectDynamicRecord, f as itemIsDone, g as uniqueEmbedTargets, h as uniqueBacklinkSources, i as ownProp, n as deriveAll, o as firstDateField, s as resolveIcon, t as defangForPrompt, v as backlinkRows, x as rollupValue, y as coerceNumeric } from "./promptSafety-CSdyp7RF.js";
6
6
  import { B as SCHEMA_FILE$1, C as CollectionQueryZ, I as parseAppManifest, J as isBackendUnavailable, K as safeSlugName, O as isRegularFile, S as compileJsonlQuery, U as resolveDataDir, V as isContainedInRoot, W as resolveTemplatePath, X as archiveDir, b as queryCsv, c as resolveMutateSet, d as storeFor, f as checkpointSqliteDatabase, ht as stagingSkillDir, i as resolvePrimaryField, it as isPresetSlug$1, j as resolveCreateItemId, m as cacheDir, n as discoverCollections, nt as getWorkspaceRoot, ot as log, r as loadCollection, s as CollectionSchemaZ, u as readOnlyRefusal, y as normalizeCsvValue } from "./discovery-B9zdNkgW.js";
7
+ import { r as isSafeCustomViewPath } from "./templatePath-k_WNbL_Q.js";
7
8
  import { ingestStatePath } from "./feeds/paths.js";
8
9
  import { mirrorSkillWrite } from "./skill-bridge/index.js";
9
10
  import path from "node:path";
@@ -83,6 +84,14 @@ var CollectionConfigZ = z.object({
83
84
  * declaration with the role and no field is refused (`assigneeProblems`),
84
85
  * because that member would silently hold nothing. */
85
86
  assigneeField: z.string().trim().min(1).optional(),
87
+ /** This collection is the public projection of `mirrorOf` — the other
88
+ * half of `public.submit[...].mirror`, declared here because the rules
89
+ * read it when the PROJECTION is written rather than when the record is.
90
+ *
91
+ * What it buys: `state` may be written by anybody, and only to the value
92
+ * the authority actually says, so a visitor who was refused a slot can
93
+ * repair the stale row that offered it to them. */
94
+ mirrorOf: NameZ.optional(),
86
95
  peerVisibility: z.enum(["public", "hidden"]).optional(),
87
96
  revealGated: z.boolean().optional(),
88
97
  gatedFrom: NameZ.optional(),
@@ -123,10 +132,38 @@ var WindowRefZ = z.object({
123
132
  collection: NameZ,
124
133
  field: z.string().trim().min(1)
125
134
  }).strict();
135
+ /** The closing bound's per-record twin, and it ships WITH `fromField` rather
136
+ * than as a symmetric extra: a booking desk that opens per slot and never
137
+ * closes is not a booking desk. Same shape, opposite comparison — and
138
+ * EXCLUSIVE where `fromField` is inclusive, so one slot's closing instant and
139
+ * the next one's opening instant may be the same number. */
126
140
  var WindowZ = z.object({
127
141
  from: z.iso.datetime().optional(),
128
142
  until: z.iso.datetime().optional(),
129
- fromField: WindowRefZ.optional()
143
+ fromField: WindowRefZ.optional(),
144
+ untilField: WindowRefZ.optional()
145
+ }).strict();
146
+ /** Which record a `field` document id must name, and what state it must be in.
147
+ *
148
+ * `idFrom: "field"` alone only stops the same string being written twice —
149
+ * nothing stops a client bypassing the page and inventing a slot, so the
150
+ * rules check the referenced record themselves. `exists()` is a FLOOR: a
151
+ * cancelled slot and a slot nobody may book any more exist too, which is what
152
+ * `where` is for.
153
+ *
154
+ * Always the object form, never a bare collection name. Two shapes for one
155
+ * key is the kind of thing a generator gets right once and wrong afterwards,
156
+ * and the rules read `s.idIn.collection` either way. */
157
+ var IdInZ = z.object({
158
+ collection: NameZ,
159
+ where: z.object({
160
+ field: z.string().trim().min(1),
161
+ equals: z.union([
162
+ z.string(),
163
+ z.number(),
164
+ z.boolean()
165
+ ])
166
+ }).strict().optional()
130
167
  }).strict();
131
168
  var ValidateZ = z.object({
132
169
  required: z.array(z.string().trim().min(1)).optional(),
@@ -151,12 +188,31 @@ var SubmitZ = z.object({
151
188
  emailField: z.string().trim().min(1).optional(),
152
189
  createFields: z.array(z.string().trim().min(1)).min(1),
153
190
  initialStatus: z.string().trim().min(1).optional(),
191
+ /** `field` is the mode that makes a CONTESTED resource exclusive: the
192
+ * booking's document id IS the slot's id, so the second person to want
193
+ * that slot is writing a document that already exists — an update, which
194
+ * the public submission path never allows. Firestore decides that
195
+ * atomically, so unlike a countable capacity (see `stampField`) this is
196
+ * first-come ENFORCED rather than first-come read off a rank. */
154
197
  idFrom: z.enum([
155
198
  "auto",
156
199
  "auth.uid",
157
- "auth.uid+field"
200
+ "auth.uid+field",
201
+ "field"
158
202
  ]).optional(),
159
203
  idField: z.string().trim().min(1).optional(),
204
+ /** Required by `idFrom: "field"` — see {@link IdInZ}. */
205
+ idIn: IdInZ.optional(),
206
+ /** The collection holding this record's PUBLIC PROJECTION, one row per
207
+ * contested thing, sharing its document id.
208
+ *
209
+ * A booking carries a name, an address and a phone number, and Firestore
210
+ * rules cannot hide a field, so the public page must not read bookings at
211
+ * all. It reads the projection instead, whose `state` is a copy of "does
212
+ * a booking with this id exist" — and the rules accept the two writes
213
+ * only as one batch, in both directions, so the copy cannot drift into
214
+ * advertising a slot that is gone. */
215
+ mirror: NameZ.optional(),
160
216
  validate: ValidateZ.optional(),
161
217
  window: WindowZ.optional(),
162
218
  /** A field the rules PIN to the server clock on create: the record must
@@ -188,6 +244,24 @@ var PublicZ = z.object({
188
244
  * well as its own declaration. */
189
245
  enabled: z.boolean().optional(),
190
246
  read: z.array(NameZ).optional(),
247
+ /** The page the public sees, instead of the generated form.
248
+ *
249
+ * A form is enough to ANSWER something and not enough to CHOOSE from
250
+ * what is available — a stylist-by-hour grid is not the far end of a
251
+ * table. So the app may name one HTML file, which the host publishes to
252
+ * `config/view` and the public page renders in a sandboxed iframe.
253
+ *
254
+ * `submit` stays declared alongside: the view sends an INTENT, and the
255
+ * page it is embedded in performs the write against these rules.
256
+ *
257
+ * `collections` is declared rather than inferred from `read`. Inferring
258
+ * it produces the worst failure this feature has — the view renders, the
259
+ * data it wanted was never sent, and it draws an empty grid with no error
260
+ * anywhere. */
261
+ view: z.object({
262
+ path: z.string().trim().min(1),
263
+ collections: z.array(NameZ).min(1)
264
+ }).strict().optional(),
191
265
  submit: z.record(NameZ, SubmitZ).optional()
192
266
  }).strict();
193
267
  /** The URL name an app is handed out under: `https://<host>/{slug}`.
@@ -294,7 +368,8 @@ function windowMillis(window) {
294
368
  if (Object.values(out).some((value) => !Number.isFinite(value))) throw new Error(`publish: window bound is not a parseable timestamp (${JSON.stringify(window)})`);
295
369
  const projected = {
296
370
  ...out,
297
- ...window.fromField === void 0 ? {} : { fromField: window.fromField }
371
+ ...window.fromField === void 0 ? {} : { fromField: window.fromField },
372
+ ...window.untilField === void 0 ? {} : { untilField: window.untilField }
298
373
  };
299
374
  return Object.keys(projected).length > 0 ? projected : void 0;
300
375
  }
@@ -370,6 +445,7 @@ function projectApp(authored, schemas, stamp, existing) {
370
445
  enabled: authored.public?.enabled === true,
371
446
  read: authored.public?.read ?? [],
372
447
  submit,
448
+ ...authored.public?.view === void 0 ? {} : { view: { collections: authored.public.view.collections } },
373
449
  publishedAt: stamp.publishedAt
374
450
  };
375
451
  if (authored.name !== void 0) config.name = authored.name;
@@ -667,7 +743,7 @@ function ruleReadFields(submit) {
667
743
  field: submit.emailField,
668
744
  why: `public.submit.<cid>.emailField — the rules compare it to the submitter's verified address`
669
745
  });
670
- if (submit.idFrom === "auth.uid+field" && submit.idField !== void 0) fields.push({
746
+ if ((submit.idFrom === "auth.uid+field" || submit.idFrom === "field") && submit.idField !== void 0) fields.push({
671
747
  field: submit.idField,
672
748
  why: `public.submit.<cid>.idField — the rules rebuild the document id from it`
673
749
  });
@@ -690,10 +766,58 @@ function submitCoherenceProblems(app, cid, submit) {
690
766
  const collection = app.collections?.[cid];
691
767
  const problems = [...statusCoherenceProblems(cid, submit, collection), ...createFieldProblems(cid, submit)];
692
768
  if (submit.idFrom === "auth.uid+field" && submit.idField === void 0) problems.push(`public.submit.${cid}.idFrom is "auth.uid+field" but no idField is declared: the rules rebuild the document id from that field and refuse every create.`);
769
+ problems.push(...fieldIdProblems(cid, submit));
693
770
  if ((submit.selfUpdate !== void 0 || submit.selfTransitions !== void 0) && !collection?.statusField) problems.push(`public.submit.${cid}.selfUpdate / selfTransitions are declared per CURRENT STATUS, but collections.${cid} declares no statusField: the rules read the current status first and refuse every self-edit without it.`);
694
771
  if (submit.audience === "participant" && Object.keys(app.members).length === 0) problems.push(`public.submit.${cid}.audience is "participant" but the roster is empty: the rules resolve the submitter's role from members, so every submission is refused.`);
695
772
  return problems;
696
773
  }
774
+ /** `idFrom: "field"` makes the document id a CLAIM ABOUT ANOTHER RECORD, and
775
+ * the claim is only worth what is checked.
776
+ *
777
+ * `idIn` is required rather than optional, and that is the whole point of
778
+ * refusing here: without it the id is any string a stranger likes, so the app
779
+ * quietly accepts bookings for slots that do not exist. Nothing downstream
780
+ * ever notices — the booking is real, its slot is not — which is exactly the
781
+ * kind of hole a gate is for and a rule cannot state.
782
+ *
783
+ * `idIn` without the mode is refused for the opposite reason: the rules read
784
+ * it only in that branch, so an author who wrote it believes a check is
785
+ * running that is not. */
786
+ function fieldIdProblems(cid, submit) {
787
+ const problems = [];
788
+ if (submit.idFrom === "field") {
789
+ if (submit.idField === void 0) problems.push(`public.submit.${cid}.idFrom is "field" but no idField is declared: the rules take the document id from that field and refuse every create.`);
790
+ if (submit.idIn === void 0) problems.push(`public.submit.${cid}.idFrom is "field" but no idIn is declared: the document id is then any string a submitter chooses, so the app accepts records pointing at things that do not exist. Name the collection the id must be found in — and, when only some of those records may be claimed, the state they must be in: "idIn": { "collection": "slots", "where": { "field": "state", "equals": "open" } }.`);
791
+ } else if (submit.idIn !== void 0) {
792
+ const mode = submit.idFrom === void 0 ? "absent" : JSON.stringify(submit.idFrom);
793
+ problems.push(`public.submit.${cid}.idIn is declared but idFrom is ${mode}: the rules read idIn only for idFrom "field", so as written nothing checks the referenced record and the declaration promises a check it does not perform.`);
794
+ }
795
+ return problems;
796
+ }
797
+ /** Every `idIn` target, checked against the collections this repository has.
798
+ *
799
+ * Separate from {@link fieldIdProblems} for the reason the file is split at
800
+ * all: that one reads the declaration alone, this one needs to know what
801
+ * exists. */
802
+ function idTargetProblems(app, collections) {
803
+ const known = new Set(collections.map((collection) => collection.cid));
804
+ const names = known.size > 0 ? [...known].sort().join(", ") : "(none)";
805
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => idInTargetProblems(cid, submit, known, names));
806
+ }
807
+ /** Where a `field` id says its record must be found.
808
+ *
809
+ * A typo passes every other check: the rules look the record up in a
810
+ * collection that does not exist, the lookup can never succeed, and every
811
+ * submission is refused with no explanation anywhere. A collection pointing
812
+ * at ITSELF is worse than a typo — on a create the document being written
813
+ * does not exist yet, so it is a declaration that can never accept anything. */
814
+ function idInTargetProblems(cid, submit, known, names) {
815
+ const target = submit.idIn?.collection;
816
+ if (target === void 0) return [];
817
+ if (target === cid) return [`public.submit.${cid}.idIn.collection names '${cid}' itself: a create writes a document that does not exist yet, so the record can never be found and every submission is refused. Name the collection of the thing being claimed (the slots, the seats, the assets).`];
818
+ if (!known.has(target)) return [`public.submit.${cid}.idIn.collection names '${target}', which is not a shared collection in this repository. The rules look the record up there, so nothing can ever be submitted. Shared collections here: ${names}.`];
819
+ return [];
820
+ }
697
821
  /** The staged reveal reads its flag off the PARENT record, so the path to that
698
822
  * parent is not optional decoration — without it the gate never opens. */
699
823
  function gateCoherenceProblems(cid, collection) {
@@ -744,7 +868,10 @@ function publishProblems(app, collections, publisherEmail) {
744
868
  ...primaryKeyProblems(app, collections),
745
869
  ...assigneeProblems(app),
746
870
  ...stampProblems(app),
747
- ...windowRefProblems(app, collections)
871
+ ...windowRefProblems(app, collections),
872
+ ...idTargetProblems(app, collections),
873
+ ...mirrorProblems(app, collections),
874
+ ...publicViewProblems(app, collections)
748
875
  ];
749
876
  }
750
877
  /** A public submission must NOT be allowed to name its own primary key.
@@ -825,14 +952,74 @@ function stampProblems(app) {
825
952
  * good. */
826
953
  function windowRefProblems(app, collections) {
827
954
  const known = new Set(collections.map((collection) => collection.cid));
828
- return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
829
- const ref = submit.window?.fromField;
830
- if (ref === void 0) return [];
831
- const problems = [];
832
- 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)"}.`);
833
- 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.`);
834
- return problems;
835
- });
955
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => [...windowBoundProblems(cid, submit, known, "fromField", submit.window?.fromField, "opening"), ...windowBoundProblems(cid, submit, known, "untilField", submit.window?.untilField, "closing")]);
956
+ }
957
+ /** Both bounds, checked identically. `untilField` arrived with the booking
958
+ * desk and reads exactly like its twin, so a check that knew only about
959
+ * `fromField` would let the closing half through unchecked — and a closing
960
+ * bound that names nothing does not leave the door ajar, it refuses every
961
+ * submission with no explanation. */
962
+ function windowBoundProblems(cid, submit, known, key, ref, which) {
963
+ if (ref === void 0) return [];
964
+ const problems = [];
965
+ if (!known.has(ref.collection)) problems.push(`public.submit.${cid}.window.${key}.collection names '${ref.collection}', which is not a shared collection in this repository. The rules read the ${which} time off a record there, so nothing can ever be submitted. Shared collections here: ${known.size > 0 ? [...known].sort().join(", ") : "(none)"}.`);
966
+ if (!submit.createFields.includes(ref.ref)) problems.push(`public.submit.${cid}.window.${key}.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.`);
967
+ return problems;
968
+ }
969
+ /** The two halves of a mirror, checked as the pair they only work as.
970
+ *
971
+ * `mirror` on the submission and `mirrorOf` on the projection are separate
972
+ * keys in separate places, and each is inert without the other: a booking
973
+ * whose slot declares no `mirrorOf` can never be created (the rules demand a
974
+ * paired write that the projection's own rule will refuse), and a projection
975
+ * whose authority declares no `mirror` drifts unbounded because nothing makes
976
+ * the two move together. Both failures are silent, and one of them —
977
+ * advertising a slot somebody already holds — is the exact thing the mirror
978
+ * exists to prevent.
979
+ *
980
+ * Also refuses a collection mirroring ITSELF, which reads as a typo and
981
+ * behaves as an unwritable collection: every create would have to prove its
982
+ * own document is simultaneously taken and open. */
983
+ function mirrorProblems(app, collections) {
984
+ const known = new Set(collections.map((collection) => collection.cid));
985
+ const names = known.size > 0 ? [...known].sort().join(", ") : "(none)";
986
+ return [...Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => mirrorClaimProblems(app, cid, submit, known, names)), ...Object.entries(app.collections ?? {}).flatMap(([cid, collection]) => mirrorOfProblems(app, cid, collection, known, names))];
987
+ }
988
+ /** The submission side: `public.submit[cid].mirror`. */
989
+ function mirrorClaimProblems(app, cid, submit, known, names) {
990
+ const { mirror } = submit;
991
+ if (mirror === void 0) return [];
992
+ if (mirror === cid) return [`public.submit.${cid}.mirror names its own collection: the projection is a SEPARATE record, and as written no create can satisfy the rules.`];
993
+ if (!known.has(mirror)) return [`public.submit.${cid}.mirror names '${mirror}', which is not a shared collection in this repository. The rules require the projection to move in the same write, so every submission is refused. Shared collections here: ${names}.`];
994
+ if (app.collections?.[mirror]?.mirrorOf !== cid) return [`public.submit.${cid}.mirror names '${mirror}', but collections.${mirror} does not declare mirrorOf: "${cid}". The two halves only work as a pair — the submission side demands the projection move with it, and the projection side is what allows that move — so as written every submission is refused.`];
995
+ return [];
996
+ }
997
+ /** The projection side: `collections[cid].mirrorOf`. */
998
+ function mirrorOfProblems(app, cid, collection, known, names) {
999
+ const authority = collection.mirrorOf;
1000
+ if (authority === void 0) return [];
1001
+ if (!known.has(authority)) return [`collections.${cid}.mirrorOf names '${authority}', which is not a shared collection in this repository. Nothing can then be true of it, so the projection's state may never be written. Shared collections here: ${names}.`];
1002
+ if (app.public?.submit?.[authority]?.mirror !== cid) return [`collections.${cid}.mirrorOf names '${authority}', but public.submit.${authority} does not declare mirror: "${cid}". Only the pair keeps the projection honest: without the other half a record can be created without moving this one, and the public page goes on offering something that is already taken.`];
1003
+ return [];
1004
+ }
1005
+ /** What the public view is handed. Declared, never inferred — a view whose
1006
+ * datasets were guessed from `public.read` renders perfectly and draws an
1007
+ * empty grid, with nothing in the page, the rules or the log to say why. */
1008
+ function publicViewProblems(app, collections) {
1009
+ const view = app.public?.view;
1010
+ if (view === void 0) return [];
1011
+ const known = new Set(collections.map((collection) => collection.cid));
1012
+ const readable = new Set(app.public?.read ?? []);
1013
+ const problems = [];
1014
+ if (!isSafeCustomViewPath(view.path) || view.path.split("/").length !== 2) problems.push(`public.view.path is '${view.path}': a published view is exactly one HTML file directly inside the collection's own views/ directory (e.g. views/booking.html) — no sub-directories, and no segments that climb out of it. The host reads this as a file to publish, and what it publishes is world-readable.`);
1015
+ for (const cid of view.collections) {
1016
+ if (!known.has(cid)) {
1017
+ problems.push(`public.view.collections names '${cid}', which is not a shared collection in this repository. Shared collections here: ${known.size > 0 ? [...known].sort().join(", ") : "(none)"}.`);
1018
+ continue;
1019
+ }
1020
+ if (!readable.has(cid)) problems.push(`public.view.collections names '${cid}', which is not in public.read: the page reads these with the VISITOR's permissions, so the rules refuse the read and the view draws an empty page. Nothing errors — this is the failure that looks like a working view with no data.`);
1021
+ }
1022
+ return problems;
836
1023
  }
837
1024
  /** What publish will actually promote, checked as the PAIR it becomes.
838
1025
  *
@@ -861,12 +1048,146 @@ function windowRefProblems(app, collections) {
861
1048
  function promotedRoleProblems(app, staged) {
862
1049
  const promoted = stagedRuleConfig(staged).collections ?? {};
863
1050
  const stagedCids = new Set(staged.map((entry) => entry.cid));
1051
+ return [
1052
+ ...promotedAssigneeProblems(app, promoted, stagedCids),
1053
+ ...promotedMirrorProblems(app, promoted, stagedCids),
1054
+ ...promotedRefFieldProblems(app, staged)
1055
+ ];
1056
+ }
1057
+ /** The FIELDS a rule reads off another record — `idIn.where.field` and the two
1058
+ * window bounds — checked against the schema publish is about to promote.
1059
+ *
1060
+ * These are checked here rather than in `publishProblems` because that gate
1061
+ * is given a cid and a primary key per collection and nothing else, on
1062
+ * purpose: it reads the DECLARATION. A field name can only be judged against
1063
+ * a schema, and the schema that matters is the STAGED one — the version
1064
+ * publish promotes — not whatever the working tree says now.
1065
+ *
1066
+ * What a typo costs: `where: { field: "staet" }` publishes cleanly, the
1067
+ * rules' comparison can never match, and every submission is denied with no
1068
+ * message. The author's own app looks broken with nothing to read.
1069
+ *
1070
+ * Only fields the schema DECLARES are accepted. A record may carry more than
1071
+ * its schema does, but a shared collection's records are written through it,
1072
+ * and "the field exists on some rows" is not something a gate can promise. */
1073
+ function promotedRefFieldProblems(app, staged) {
1074
+ const schemaOf = new Map(staged.map((entry) => [entry.cid, entry.doc.publishedSchema]));
1075
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => submitRefProblems(schemaOf, cid, submit));
1076
+ }
1077
+ function submitRefProblems(schemaOf, cid, submit) {
1078
+ return [
1079
+ ...idInRefProblems(schemaOf, cid, submit),
1080
+ ...boundRefProblems(schemaOf, cid, "fromField", submit.window?.fromField),
1081
+ ...boundRefProblems(schemaOf, cid, "untilField", submit.window?.untilField)
1082
+ ];
1083
+ }
1084
+ function idInRefProblems(schemaOf, cid, submit) {
1085
+ const where = submit.idIn?.where;
1086
+ if (where === void 0) return [];
1087
+ return [...refFieldProblem(schemaOf, cid, "idIn.where.field", submit.idIn?.collection, where.field), ...comparableProblem(schemaOf, cid, submit.idIn?.collection, where)];
1088
+ }
1089
+ function boundRefProblems(schemaOf, cid, key, ref) {
1090
+ if (ref === void 0) return [];
1091
+ return [...refFieldProblem(schemaOf, cid, `window.${key}.field`, ref.collection, ref.field), ...millisProblem(schemaOf, cid, `window.${key}.field`, ref)];
1092
+ }
1093
+ /** The field spec a reference points at, or undefined when there is nothing
1094
+ * staged to judge it against (the host refuses that separately, naming every
1095
+ * missing collection at once). */
1096
+ function referencedField(schemaOf, target, field) {
1097
+ if (target === void 0 || field === void 0) return void 0;
1098
+ return schemaOf.get(target)?.fields?.[field];
1099
+ }
1100
+ /** An enum's domain, or undefined for every other kind. Narrowed by the key
1101
+ * rather than asserted: `fields` is a discriminated union and only some of
1102
+ * its members carry `values`. */
1103
+ function enumValues(spec) {
1104
+ return spec.type === "enum" ? spec.values : void 0;
1105
+ }
1106
+ function refFieldProblem(schemaOf, cid, key, target, field) {
1107
+ if (target === void 0 || field === void 0) return [];
1108
+ const schema = schemaOf.get(target);
1109
+ if (schema === void 0 || referencedField(schemaOf, target, field) !== void 0) return [];
1110
+ const known = Object.keys(schema.fields ?? {}).sort().join(", ");
1111
+ return [`public.submit.${cid}.${key} names '${field}', which the STAGED schema of '${target}' — the one publish promotes — does not declare. The rules read that field off the record and compare it, so as written every submission is refused with nothing to explain it. Fields on '${target}': ${known.length > 0 ? known : "(none)"}.`];
1112
+ }
1113
+ /** A comparison the rules can never satisfy is as dead as a missing field, and
1114
+ * looks even more correct on the page: an `enum` whose domain does not contain
1115
+ * the value, or a boolean field compared with a string. */
1116
+ function comparableProblem(schemaOf, cid, target, where) {
1117
+ const spec = referencedField(schemaOf, target, where.field);
1118
+ if (spec === void 0) return [];
1119
+ const said = JSON.stringify(where.equals);
1120
+ const values = enumValues(spec);
1121
+ if (values !== void 0) {
1122
+ if (values.includes(String(where.equals))) return [];
1123
+ return [`public.submit.${cid}.idIn.where.equals is ${said}, which is not one of the values '${where.field}' can hold on '${String(target)}' (${values.join(", ") || "(none)"}). The comparison can never be true, so every submission is refused.`];
1124
+ }
1125
+ const wanted = spec.type === "number" ? "number" : spec.type === "boolean" ? "boolean" : "string";
1126
+ if (typeof where.equals === wanted) return [];
1127
+ return [`public.submit.${cid}.idIn.where.equals is ${said}, and '${where.field}' on '${String(target)}' is a ${spec.type} field. The rules compare the stored value with this one and never coerce, so the comparison can never be true and every submission is refused.`];
1128
+ }
1129
+ /** A per-record window bound is EPOCH MILLIS, because the rules have no date
1130
+ * arithmetic and do not coerce: they compare `request.time.toMillis()` with
1131
+ * whatever is stored. A `datetime` field holds an ISO string, which is a type
1132
+ * error that fails closed — the window never opens, and nothing says so. */
1133
+ function millisProblem(schemaOf, cid, key, ref) {
1134
+ const spec = referencedField(schemaOf, ref?.collection, ref?.field);
1135
+ if (spec === void 0 || spec.type === "number") return [];
1136
+ return [`public.submit.${cid}.${key} names '${String(ref?.field)}' on '${String(ref?.collection)}', which is a ${spec.type} field. A per-record bound is EPOCH MILLIS: the rules compare it with request.time.toMillis() and never coerce, so anything else is a type error that refuses every submission — the window simply never opens. Store the instant as a number.`];
1137
+ }
1138
+ function promotedAssigneeProblems(app, promoted, stagedCids) {
864
1139
  return Object.entries(app.members).flatMap(([email, roles]) => Object.entries(roles).flatMap(([cid, role]) => {
865
1140
  if (role !== "assignee" || cid === "*" || !stagedCids.has(cid)) return [];
866
1141
  if (promoted[cid]?.assigneeField !== void 0) return [];
867
1142
  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.`];
868
1143
  }));
869
1144
  }
1145
+ /** The mirror's other half, checked against what publish will actually
1146
+ * promote — the same trap as the assignee's field, reached by the same route.
1147
+ *
1148
+ * `mirror` is published from the MANIFEST (it lives in `public.submit`) while
1149
+ * `mirrorOf` is promoted from what DEPLOY staged. Add both halves to
1150
+ * `app.json` and publish without redeploying, and what lands is a submission
1151
+ * demanding a paired projection write beside a projection whose rule config
1152
+ * does not allow it: every booking is refused, and the declaration on disk
1153
+ * looks perfectly sound.
1154
+ *
1155
+ * Refused in the reverse direction too. Removing `mirrorOf` from a live app
1156
+ * and publishing without a deploy leaves the projection accepting nothing —
1157
+ * and removing it FROM the staged side while the submission still demands it
1158
+ * is the drift this pair exists to prevent. */
1159
+ function promotedMirrorProblems(app, promoted, stagedCids) {
1160
+ return [...addedMirrorProblems(app, promoted, stagedCids), ...strandedMirrorProblems(app, promoted)];
1161
+ }
1162
+ /** The manifest asks for a projection the promoted configuration will not
1163
+ * allow: every submission denied, and nothing on the page to say why. */
1164
+ function addedMirrorProblems(app, promoted, stagedCids) {
1165
+ return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
1166
+ const { mirror } = submit;
1167
+ if (mirror === void 0 || !stagedCids.has(mirror)) return [];
1168
+ if (promoted[mirror]?.mirrorOf === cid) return [];
1169
+ return [`public.submit.${cid}.mirror names '${mirror}', and the STAGED version of '${mirror}' — the one publish promotes — does not declare mirrorOf: "${cid}", even if app.json declares it now. Publish writes the submission side from app.json and the collection side from the deploy, so what lands is a booking that must move its projection beside a projection that refuses to move: every submission is denied, with nothing on the page to say why. Run deploy again, so the version being published is the one the declaration describes.`];
1170
+ });
1171
+ }
1172
+ /** The other direction, and the DANGEROUS one.
1173
+ *
1174
+ * Take a live pair, delete BOTH halves from `app.json`, and publish without
1175
+ * redeploying. The submission side comes from the manifest, so nothing
1176
+ * requires the projection to move any more; the collection side comes from
1177
+ * staging, which still says `mirrorOf`, so the projection stays writable.
1178
+ * Bookings are then created while the public row goes on saying `open` — the
1179
+ * precise failure the pair exists to prevent, arrived at by removing it.
1180
+ *
1181
+ * Refused rather than tolerated because the app keeps WORKING: submissions
1182
+ * succeed. Only the public page is wrong, and only to the people reading it. */
1183
+ function strandedMirrorProblems(app, promoted) {
1184
+ return Object.entries(promoted).flatMap(([cid, config]) => {
1185
+ const authority = config.mirrorOf;
1186
+ if (authority === void 0) return [];
1187
+ if (app.public?.submit?.[authority]?.mirror === cid) return [];
1188
+ return [`the STAGED version of '${cid}' — the one publish promotes — declares mirrorOf: "${authority}", and app.json no longer declares public.submit.${authority}.mirror: "${cid}". What lands is a projection that is still writable beside submissions that no longer have to move it, so records can be created while '${cid}' goes on advertising them as available. Nothing fails: the app works and the public page lies. Run deploy again, so the version being published is the one the declaration describes.`];
1189
+ });
1190
+ }
870
1191
  //#endregion
871
1192
  //#region src/collection/server/skillAssets.ts
872
1193
  /** Read a collection's custom-view HTML, path-safely. `viewFile` is a
@@ -2939,4 +3260,4 @@ function makeManageCollectionTool(deps = {}) {
2939
3260
  //#endregion
2940
3261
  export { AuthoredAppZ 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, projectDeploy as J, appStagingPath as K, bindsSubmitterIdentity as L, buildCollectionActionSeedPrompt as M, promptPathsFor as N, runCollectionQuery as O, readCustomViewHtml as P, APP_ROLES as Q, promotedRoleProblems as R, MAX_RECORD_ISSUES as S, validateRecordObject as T, appConfigPath as U, APP_SLUGS_COLLECTION as V, appSchemasPath as W, promoteSchema as X, projectPublish as Y, stagedRuleConfig as Z, computeCollectionIcon as _, deleteCollection as a, applyMutateAction as b, computeSuccessor as c, isTriggerDue as d, parseAuthoredApp as et, maybeSpawnSuccessor as f, ONE_SECOND_MS as g, successorId as h, deleteCustomView as i, buildActionSeedPrompt as j, enrichItems as k, daysInMonth as l, resolveEvery as m, MAX_UNSELECTIVE_ITEMS as n, deleteCollectionRefusalMessage as o, parseCivil as p, projectApp as q, makeManageCollectionTool as r, advanceTriggerDate as s, MAX_SCHEMA_ISSUES as t, formatCivil as u, buildWorkspaceOntology as v, validateCollectionRecords as w, firstMutateParamProblem as x, schemaRelations as y, publishProblems as z };
2941
3262
 
2942
- //# sourceMappingURL=server-DkT40KgR.js.map
3263
+ //# sourceMappingURL=server-DGjmK9vU.js.map