@mulmoclaude/core 3.4.0 → 3.6.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.
Files changed (63) hide show
  1. package/assets/helps/collection-skills.md +49 -1
  2. package/assets/helps/error-recovery.md +53 -0
  3. package/dist/{calendarGrid-Csy2rpjp.js → calendarGrid-CQ8MVSRb.js} +141 -2
  4. package/dist/calendarGrid-CQ8MVSRb.js.map +1 -0
  5. package/dist/{calendarGrid-p8K-7cuB.cjs → calendarGrid-DGILaVxI.cjs} +194 -1
  6. package/dist/calendarGrid-DGILaVxI.cjs.map +1 -0
  7. package/dist/collection/core/collectionKey.d.ts +67 -0
  8. package/dist/collection/core/schema.d.ts +8 -1
  9. package/dist/collection/core/schemaZ.d.ts +36 -26
  10. package/dist/collection/firestore.cjs +51 -0
  11. package/dist/collection/firestore.cjs.map +1 -0
  12. package/dist/collection/firestore.d.ts +1 -0
  13. package/dist/collection/firestore.js +50 -0
  14. package/dist/collection/firestore.js.map +1 -0
  15. package/dist/collection/index.cjs +11 -2
  16. package/dist/collection/index.d.ts +1 -0
  17. package/dist/collection/index.js +3 -3
  18. package/dist/collection/registry/server/index.cjs +2 -2
  19. package/dist/collection/registry/server/index.js +2 -2
  20. package/dist/collection/server/appManifest.d.ts +53 -0
  21. package/dist/collection/server/delete.d.ts +10 -0
  22. package/dist/collection/server/discoveredCollection.d.ts +10 -0
  23. package/dist/collection/server/discovery.d.ts +1 -0
  24. package/dist/collection/server/firestoreDocs.d.ts +39 -0
  25. package/dist/collection/server/firestoreStore.d.ts +14 -0
  26. package/dist/collection/server/host.d.ts +90 -8
  27. package/dist/collection/server/index.cjs +13 -3
  28. package/dist/collection/server/index.d.ts +4 -1
  29. package/dist/collection/server/index.js +4 -4
  30. package/dist/collection-watchers/index.cjs +161 -49
  31. package/dist/collection-watchers/index.cjs.map +1 -1
  32. package/dist/collection-watchers/index.js +161 -49
  33. package/dist/collection-watchers/index.js.map +1 -1
  34. package/dist/collection-watchers/reconciler.d.ts +36 -1
  35. package/dist/{discovery-D7LCN8gJ.js → discovery-CdkaURVY.js} +411 -57
  36. package/dist/discovery-CdkaURVY.js.map +1 -0
  37. package/dist/{discovery-CrA_EqUJ.cjs → discovery-Cw5xWzZD.cjs} +469 -55
  38. package/dist/discovery-Cw5xWzZD.cjs.map +1 -0
  39. package/dist/feeds/index.cjs +2 -2
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/server/index.cjs +4 -4
  42. package/dist/feeds/server/index.js +4 -4
  43. package/dist/google/index.cjs +2 -2
  44. package/dist/google/index.js +2 -2
  45. package/dist/{ingestTypes-F2IJhOLJ.js → ingestTypes-CEi-Ot7F.js} +2 -2
  46. package/dist/{ingestTypes-F2IJhOLJ.js.map → ingestTypes-CEi-Ot7F.js.map} +1 -1
  47. package/dist/{ingestTypes-CPR-h-OZ.cjs → ingestTypes-iaiq33Ph.cjs} +2 -2
  48. package/dist/{ingestTypes-CPR-h-OZ.cjs.map → ingestTypes-iaiq33Ph.cjs.map} +1 -1
  49. package/dist/{promptSafety-ACXw_2Yu.js → promptSafety-CpiME8pj.js} +2 -2
  50. package/dist/{promptSafety-ACXw_2Yu.js.map → promptSafety-CpiME8pj.js.map} +1 -1
  51. package/dist/{promptSafety-CNEW3_qS.cjs → promptSafety-NNGViiCr.cjs} +2 -2
  52. package/dist/{promptSafety-CNEW3_qS.cjs.map → promptSafety-NNGViiCr.cjs.map} +1 -1
  53. package/dist/{server-CorfPQVQ.cjs → server-9mFaGLdq.cjs} +25 -6
  54. package/dist/server-9mFaGLdq.cjs.map +1 -0
  55. package/dist/{server-D6QY853r.js → server-DdHaX_Uu.js} +25 -6
  56. package/dist/server-DdHaX_Uu.js.map +1 -0
  57. package/package.json +7 -1
  58. package/dist/calendarGrid-Csy2rpjp.js.map +0 -1
  59. package/dist/calendarGrid-p8K-7cuB.cjs.map +0 -1
  60. package/dist/discovery-CrA_EqUJ.cjs.map +0 -1
  61. package/dist/discovery-D7LCN8gJ.js.map +0 -1
  62. package/dist/server-CorfPQVQ.cjs.map +0 -1
  63. package/dist/server-D6QY853r.js.map +0 -1
@@ -1,5 +1,7 @@
1
1
  import { canonicalRoot } from '../../files/root.js';
2
+ import { CollectionKey } from '../core/collectionKey.js';
2
3
  import { StructuredLogger } from '../../host/hostSlot.js';
4
+ import { FirestoreDocs } from './firestoreDocs';
3
5
  /** Public alias of the shared `StructuredLogger` — keeps the domain surface name-stable. */
4
6
  export type CollectionLogger = StructuredLogger;
5
7
  /** Re-exported so collection-engine callers (the watcher, the reconciler) keep
@@ -12,6 +14,10 @@ export { canonicalRoot };
12
14
  * watcher is already running — and a host catching both in one place should
13
15
  * not have to match on message text to tell them apart. */
14
16
  export declare const COLLECTION_ROOT_REQUIRED = "COLLECTION_ROOT_REQUIRED";
17
+ /** Build a local {@link CollectionKey}, canonicalising the root. The identity
18
+ * type itself is isomorphic and cannot canonicalise (that needs `node:path`),
19
+ * so this is the constructor server code should use. */
20
+ export declare const localCollectionKey: (root: string, slug: string) => CollectionKey;
15
21
  /** INVARIANT — **a slug is unique within a root and nowhere else.**
16
22
  *
17
23
  * A collection's identity is `(root, slug)`. Anything keyed by slug ALONE —
@@ -91,30 +97,91 @@ export interface CollectionHost {
91
97
  /** True for a preset-skill slug (host-owned naming convention). */
92
98
  isPresetSlug: (slug: string) => boolean;
93
99
  }
100
+ /** The authenticated Firestore access a shared collection is served through.
101
+ *
102
+ * It carries NO uid, and that is a decision rather than an omission. Nothing
103
+ * about a shared collection is keyed by uid any more: the documents live at
104
+ * `apps/{aid}/collections/{cid}/items`, and the deployed rules authorize on
105
+ * `request.auth.token.email` against the app's member roster. A uid kept
106
+ * "just for identity" would be a value nothing checks, sitting next to a path
107
+ * it no longer determines — which is how a later reader ends up deriving a
108
+ * path from it again.
109
+ *
110
+ * `email` is what the rules actually evaluate, so it is what makes a refusal
111
+ * explainable: `permission-denied` is the most common failure a shared
112
+ * collection has and the least informative, and naming the principal turns it
113
+ * into "signed in as a@b — this app's roster may not list you". A session with
114
+ * no verified email is NOT a session here (the accessor answers null): public
115
+ * anonymous submission is a visitor's path through the published web app, not
116
+ * the host's.
117
+ *
118
+ * `docs` is the narrow document interface rather than a raw `Firestore` so the
119
+ * backend stays testable: core ships `createFirestoreDocs` over the real SDK
120
+ * (`firestoreDocs.ts`) and tests inject an in-memory fake. */
121
+ export interface FirestoreHandle {
122
+ docs: FirestoreDocs;
123
+ email: string;
124
+ }
94
125
  /** A collection's records changed on disk. Carries the `slug` so the host can
95
126
  * publish on a per-collection channel; `ids` lists the affected record ids
96
127
  * when known (a consumer may ignore them and refetch the whole collection),
97
128
  * and `op` is advisory. Deliberately carries NO record bodies — this is a
98
129
  * "refetch" ping, not a data feed, so it stays cheap and leaks nothing when a
99
130
  * host relays it into an opaque-origin custom-view iframe. */
100
- export interface CollectionChangePayload {
131
+ /** Fields every change carries, whatever kind of collection it is about. */
132
+ interface CollectionChangeBase {
133
+ /** The collection's NAME in its scope: the slug, or a shared collection's
134
+ * `cid`. Never an identity on its own — see the two arms below. */
101
135
  slug: string;
136
+ ids?: string[];
137
+ op?: "upsert" | "delete";
138
+ }
139
+ /** A change to a collection in a directory. */
140
+ export interface LocalCollectionChange extends CollectionChangeBase {
102
141
  /** Absolute workspace/project root the change happened under. Present only
103
- * when the engine call carried an explicit `workspaceRoot`; absent means
104
- * the host's configured root, so a single-workspace host never sets it.
142
+ * when the engine call carried an explicit `workspaceRoot`; absent means the
143
+ * host's configured root, so a single-workspace host never sets it.
105
144
  * A multi-root host MUST key its live-update fan-out on `(root, slug)` —
106
- * two projects each owning a `tasks` collection would otherwise refresh
107
- * each other's open views. */
145
+ * two projects each owning a `tasks` collection would otherwise refresh each
146
+ * other's open views. */
108
147
  root?: string;
109
- ids?: string[];
110
- op?: "upsert" | "delete";
148
+ /** Never on a local change. The two arms are mutually exclusive at the TYPE
149
+ * level, not by convention: a payload carrying both would be read as shared
150
+ * by `collectionChangeKey`, which drops the root and fans the update out on
151
+ * the wrong channel. */
152
+ aid?: never;
111
153
  }
154
+ /** A change to a collection published to a shared app. */
155
+ export interface SharedCollectionChange extends CollectionChangeBase {
156
+ /** The shared app (`apps/{aid}/collections/{cid}`). Required on this arm:
157
+ * it is what makes the payload an identity. */
158
+ aid: string;
159
+ /** Never on a shared change — a shared collection has no root, because the
160
+ * same collection is resolved from every clone of the repository. */
161
+ root?: never;
162
+ }
163
+ /** A collection's records changed. Carries the `slug` so the host can publish
164
+ * on a per-collection channel; `ids` lists the affected record ids when known
165
+ * (a consumer may ignore them and refetch the whole collection), and `op` is
166
+ * advisory. Deliberately carries NO record bodies — this is a "refetch" ping,
167
+ * not a data feed, so it stays cheap and leaks nothing when a host relays it
168
+ * into an opaque-origin custom-view iframe.
169
+ *
170
+ * Reading a payload is unchanged: `slug`, `root`, `ids` and `op` are all
171
+ * reachable on the union. Use {@link collectionChangeKey} rather than deciding
172
+ * what an absent field means by hand. */
173
+ export type CollectionChangePayload = LocalCollectionChange | SharedCollectionChange;
112
174
  type CollectionChangePublisher = (payload: CollectionChangePayload) => void;
113
175
  /** Build a change payload, attaching `root` only when the engine call carried
114
176
  * an explicit one. Centralised so every publish site states the root the same
115
177
  * way, and so a single-workspace host's payload shape stays byte-identical to
116
178
  * what it saw before multi-root support. */
117
- export declare function collectionChangePayload(base: Omit<CollectionChangePayload, "root">, root: string | undefined): CollectionChangePayload;
179
+ export declare function collectionChangePayload(base: CollectionChangeBase, root: string | undefined): LocalCollectionChange;
180
+ /** Build a change payload for a SHARED collection. `slug` carries the `cid`,
181
+ * which is what it is called inside its app; `aid` is what makes it an
182
+ * identity. Never stamps a `root` — a shared collection does not have one. */
183
+ export declare function sharedCollectionChangePayload(base: CollectionChangeBase, aid: string): SharedCollectionChange;
184
+ export declare function collectionChangeKey(payload: CollectionChangePayload, fallbackRoot: string): CollectionKey;
118
185
  /** Wire the engine to a host. Call once at server startup, before any
119
186
  * collection storage operation. Re-binding to a *different* host throws —
120
187
  * silently redirecting later filesystem operations to another workspace
@@ -132,6 +199,21 @@ export declare function setCollectionChangePublisher(publish: CollectionChangePu
132
199
  * expected to be fire-and-forget (it wraps its own pubsub call in try/catch),
133
200
  * so this stays a thin pass-through and never throws into the write. */
134
201
  export declare function publishCollectionChange(payload: CollectionChangePayload): void;
202
+ /** Wire the accessor for the host's authenticated Firestore session.
203
+ *
204
+ * Separate from `configureCollectionHost` for the same reason
205
+ * `setCollectionChangePublisher` is: the host binding is set at the top of
206
+ * server startup, but this session doesn't exist until the user connects
207
+ * remote-host (and closes again on disconnect), so it cannot be part of a
208
+ * one-shot binding. Optional — left unwired, only shared collections are
209
+ * affected, and they report "not connected". Pass `null` to detach. */
210
+ export declare function setFirestoreAccessor(accessor: (() => FirestoreHandle | null) | null): void;
211
+ /** The host's live Firestore access, or null when there is no session (or the
212
+ * host never wired one — every non-shared backend leaves it unset).
213
+ * Callers MUST surface null as an actionable "connect remote-host first",
214
+ * never as an empty result: silence would be indistinguishable from a
215
+ * collection that genuinely has no records. */
216
+ export declare function firestoreHandle(): FirestoreHandle | null;
135
217
  /** The configured workspace root. Throws if the host never configured one —
136
218
  * and, under an explicit-root binding (`workspaceRoot: null`), throws rather
137
219
  * than guessing, which is the whole point of that mode: on a multi-root host
@@ -1,9 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_root = require("../../root-rPH6FGDT.cjs");
3
- const require_calendarGrid = require("../../calendarGrid-p8K-7cuB.cjs");
4
- const require_discovery = require("../../discovery-CrA_EqUJ.cjs");
3
+ const require_calendarGrid = require("../../calendarGrid-DGILaVxI.cjs");
4
+ const require_discovery = require("../../discovery-Cw5xWzZD.cjs");
5
5
  const require_templatePath = require("../../templatePath-27vUZowm.cjs");
6
- const require_server = require("../../server-CorfPQVQ.cjs");
6
+ const require_server = require("../../server-9mFaGLdq.cjs");
7
+ exports.APP_MANIFEST_FILE = require_discovery.APP_MANIFEST_FILE;
7
8
  exports.BackendUnavailableError = require_discovery.BackendUnavailableError;
8
9
  exports.COLLECTION_ROOT_REQUIRED = require_discovery.COLLECTION_ROOT_REQUIRED;
9
10
  exports.COMPUTED_TYPES = require_calendarGrid.COMPUTED_TYPES;
@@ -17,11 +18,13 @@ exports.MAX_UNSELECTIVE_ITEMS = require_server.MAX_UNSELECTIVE_ITEMS;
17
18
  exports.SCHEMA_FILE = require_discovery.SCHEMA_FILE;
18
19
  exports.acceptParsedSchema = require_discovery.acceptParsedSchema;
19
20
  exports.advanceTriggerDate = require_server.advanceTriggerDate;
21
+ exports.appManifestReason = require_discovery.appManifestReason;
20
22
  exports.applyMutateAction = require_server.applyMutateAction;
21
23
  exports.buildActionSeedPrompt = require_server.buildActionSeedPrompt;
22
24
  exports.buildCollectionActionSeedPrompt = require_server.buildCollectionActionSeedPrompt;
23
25
  exports.buildWorkspaceOntology = require_server.buildWorkspaceOntology;
24
26
  exports.canonicalRoot = require_root.canonicalRoot;
27
+ exports.collectionChangeKey = require_discovery.collectionChangeKey;
25
28
  exports.collectionChangePayload = require_discovery.collectionChangePayload;
26
29
  exports.collectionWritable = require_discovery.collectionWritable;
27
30
  exports.compileCsvQuery = require_discovery.compileCsvQuery;
@@ -41,6 +44,7 @@ exports.deleteItem = require_discovery.deleteItem;
41
44
  exports.discoverCollections = require_discovery.discoverCollections;
42
45
  exports.encodeCsvRecordId = require_discovery.encodeCsvRecordId;
43
46
  exports.enrichItems = require_server.enrichItems;
47
+ exports.firestoreHandle = require_discovery.firestoreHandle;
44
48
  exports.firstMutateParamProblem = require_server.firstMutateParamProblem;
45
49
  exports.formatCivil = require_server.formatCivil;
46
50
  exports.generateItemId = require_discovery.generateItemId;
@@ -55,12 +59,15 @@ exports.isSafeTemplatePath = require_templatePath.isSafeTemplatePath;
55
59
  exports.isTriggerDue = require_server.isTriggerDue;
56
60
  exports.itemFilePath = require_discovery.itemFilePath;
57
61
  exports.listItems = require_discovery.listItems;
62
+ exports.loadAppManifest = require_discovery.loadAppManifest;
58
63
  exports.loadCollection = require_discovery.loadCollection;
64
+ exports.localCollectionKey = require_discovery.localCollectionKey;
59
65
  exports.log = require_discovery.log;
60
66
  exports.makeManageCollectionTool = require_server.makeManageCollectionTool;
61
67
  exports.maybeSpawnSuccessor = require_server.maybeSpawnSuccessor;
62
68
  exports.normalizeCsvValue = require_discovery.normalizeCsvValue;
63
69
  exports.pageFromFullRead = require_discovery.pageFromFullRead;
70
+ exports.parseAppManifest = require_discovery.parseAppManifest;
64
71
  exports.parseCivil = require_server.parseCivil;
65
72
  exports.peekWorkspaceRoot = require_discovery.peekWorkspaceRoot;
66
73
  exports.projectItemFields = require_discovery.projectItemFields;
@@ -83,6 +90,9 @@ exports.safeRecordId = require_discovery.safeRecordId;
83
90
  exports.safeSlugName = require_discovery.safeSlugName;
84
91
  exports.schemaRelations = require_server.schemaRelations;
85
92
  exports.setCollectionChangePublisher = require_discovery.setCollectionChangePublisher;
93
+ exports.setFirestoreAccessor = require_discovery.setFirestoreAccessor;
94
+ exports.sharedCollectionChangePayload = require_discovery.sharedCollectionChangePayload;
95
+ exports.sharedItemsPath = require_discovery.sharedItemsPath;
86
96
  exports.storeFor = require_discovery.storeFor;
87
97
  exports.successorId = require_server.successorId;
88
98
  exports.toDetail = require_discovery.toDetail;
@@ -1,4 +1,7 @@
1
- export { configureCollectionHost, canonicalRoot, getWorkspaceRoot, peekWorkspaceRoot, COLLECTION_ROOT_REQUIRED, log, setCollectionChangePublisher, publishCollectionChange, collectionChangePayload, type CollectionHost, type CollectionLogger, type CollectionChangePayload, } from './host';
1
+ export { configureCollectionHost, canonicalRoot, getWorkspaceRoot, peekWorkspaceRoot, COLLECTION_ROOT_REQUIRED, log, setCollectionChangePublisher, publishCollectionChange, collectionChangePayload, sharedCollectionChangePayload, collectionChangeKey, localCollectionKey, type CollectionHost, type CollectionLogger, type CollectionChangePayload, firestoreHandle, setFirestoreAccessor, type FirestoreHandle, type LocalCollectionChange, type SharedCollectionChange, } from './host';
2
+ export type { FirestoreDoc, FirestoreDocs } from './firestoreDocs';
3
+ export { sharedItemsPath } from './firestoreStore';
4
+ export { loadAppManifest, parseAppManifest, appManifestReason, APP_MANIFEST_FILE, type AppManifest, type AppManifestResult } from './appManifest';
2
5
  export type { LoadedCollection } from './discoveredCollection';
3
6
  export * from './paths';
4
7
  export * from './templatePath';
@@ -1,6 +1,6 @@
1
1
  import { t as canonicalRoot } from "../../root-BMroU_mB.js";
2
- import { S as COMPUTED_TYPES } from "../../calendarGrid-Csy2rpjp.js";
3
- import { A as deleteItem, B as resolveDataDir, C as compileCsvQuery, D as MAX_QUERY_ROWS, E as DEFAULT_QUERY_ROWS, F as resolveCreateItemId, H as safeRecordId, I as writeItem, J as configureCollectionHost, K as collectionChangePayload, L as SCHEMA_FILE, M as isRegularFile, N as listItems, O as BackendUnavailableError, P as readItem, Q as peekWorkspaceRoot, R as isContainedInRoot, T as CollectionQueryZ, U as safeSlugName, V as resolveTemplatePath, W as COLLECTION_ROOT_REQUIRED, Y as getWorkspaceRoot, Z as log, _ as csvRowToItem, a as toDetail, b as encodeCsvRecordId, d as storeFor, et as publishCollectionChange, h as MAX_CSV_ROWS, i as resolvePrimaryField, j as generateItemId, k as isBackendUnavailable, l as collectionWritable, m as projectItemFields, n as discoverCollections, o as toSummary, p as pageFromFullRead, r as loadCollection, s as CollectionSchemaZ, t as acceptParsedSchema, tt as setCollectionChangePublisher, u as readOnlyRefusal, v as decodeCsvRecordId, w as compileJsonlQuery, x as normalizeCsvValue, y as dedupeByRecordId, z as itemFilePath } from "../../discovery-D7LCN8gJ.js";
2
+ import { j as COMPUTED_TYPES } from "../../calendarGrid-CQ8MVSRb.js";
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 log, ct as publishCollectionChange, d as storeFor, dt as sharedCollectionChangePayload, et as configureCollectionHost, g as decodeCsvRecordId, h as csvRowToItem, i as resolvePrimaryField, it as localCollectionKey, j as resolveCreateItemId, k as listItems, l as collectionWritable, lt as setCollectionChangePublisher, n as discoverCollections, nt as getWorkspaceRoot, o as toSummary, ot as peekWorkspaceRoot, p as MAX_CSV_ROWS, q as BackendUnavailableError, r as loadCollection, s as CollectionSchemaZ, t as acceptParsedSchema, tt as firestoreHandle, u as readOnlyRefusal, ut as setFirestoreAccessor, v as encodeCsvRecordId, w as DEFAULT_QUERY_ROWS, x as compileCsvQuery, y as normalizeCsvValue, z as projectItemFields } from "../../discovery-CdkaURVY.js";
4
4
  import { i as isSafeTemplatePath, n as isSafeCustomViewI18nPath, r as isSafeCustomViewPath, t as isSafeActionTemplatePath } from "../../templatePath-k_WNbL_Q.js";
5
- import { A as buildCollectionActionSeedPrompt, C as validateRecordObject, D as enrichItems, E as runCollectionQuery, M as readCustomViewHtml, N as readCustomViewI18n, O as runQueryOverRows, P as readSkillTemplate, S as validateCollectionRecords, T as recordFieldProblem, _ 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 promptPathsFor, k as buildActionSeedPrompt, l as daysInMonth, m as resolveEvery, n as MAX_UNSELECTIVE_ITEMS, o as deleteCollectionRefusalMessage, p as parseCivil, r as makeManageCollectionTool, s as advanceTriggerDate, t as MAX_SCHEMA_ISSUES, u as formatCivil, v as buildWorkspaceOntology, w as compileRecordZ, x as firstMutateParamProblem, y as schemaRelations } from "../../server-D6QY853r.js";
6
- export { BackendUnavailableError, COLLECTION_ROOT_REQUIRED, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, canonicalRoot, collectionChangePayload, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, isBackendUnavailable, isContainedInRoot, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseCivil, peekWorkspaceRoot, projectItemFields, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolvePrimaryField, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
5
+ import { A as buildCollectionActionSeedPrompt, C as validateRecordObject, D as enrichItems, E as runCollectionQuery, M as readCustomViewHtml, N as readCustomViewI18n, O as runQueryOverRows, P as readSkillTemplate, S as validateCollectionRecords, T as recordFieldProblem, _ 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 promptPathsFor, k as buildActionSeedPrompt, l as daysInMonth, m as resolveEvery, n as MAX_UNSELECTIVE_ITEMS, o as deleteCollectionRefusalMessage, p as parseCivil, r as makeManageCollectionTool, s as advanceTriggerDate, t as MAX_SCHEMA_ISSUES, u as formatCivil, v as buildWorkspaceOntology, w as compileRecordZ, x as firstMutateParamProblem, y as schemaRelations } from "../../server-DdHaX_Uu.js";
6
+ export { APP_MANIFEST_FILE, BackendUnavailableError, COLLECTION_ROOT_REQUIRED, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, appManifestReason, applyMutateAction, 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, isBackendUnavailable, isContainedInRoot, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadAppManifest, loadCollection, localCollectionKey, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseAppManifest, parseCivil, peekWorkspaceRoot, projectItemFields, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolvePrimaryField, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, setFirestoreAccessor, sharedCollectionChangePayload, sharedItemsPath, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_root = require("../root-rPH6FGDT.cjs");
3
- const require_calendarGrid = require("../calendarGrid-p8K-7cuB.cjs");
4
- const require_promptSafety = require("../promptSafety-CNEW3_qS.cjs");
3
+ const require_calendarGrid = require("../calendarGrid-DGILaVxI.cjs");
4
+ const require_promptSafety = require("../promptSafety-NNGViiCr.cjs");
5
5
  require("../collection/index.cjs");
6
- const require_discovery = require("../discovery-CrA_EqUJ.cjs");
7
- const require_server = require("../server-CorfPQVQ.cjs");
6
+ const require_discovery = require("../discovery-Cw5xWzZD.cjs");
7
+ const require_server = require("../server-9mFaGLdq.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
@@ -55,6 +55,12 @@ var LEGACY_ID_PREFIX = "collection-completion:";
55
55
  * so the split is unambiguous. */
56
56
  var ROOT_MARK = "@";
57
57
  var ROOT_SEP = "\0";
58
+ /** Marks a key that belongs to a SHARED collection (`apps/{aid}/collections/
59
+ * {cid}`) rather than to a directory: `<prefix>#<aid>\0<cid>:<itemId>`. A
60
+ * shared collection has no root, and its `cid` may well be the slug some
61
+ * project already owns, so it needs a mark of its own — reusing the rootless
62
+ * form would make the two dedupe into one bell. */
63
+ var SHARED_MARK = "#";
58
64
  /** Stable key encoding (root, slug, item), round-tripped through the entry's
59
65
  * `pluginData` so we can find it later without a side state file. Slug +
60
66
  * itemId are upstream-validated via `safeSlugName`, which forbids the
@@ -76,21 +82,31 @@ var ROOT_SEP = "\0";
76
82
  * this id is written to disk: a caller reaching `reconcileItem` directly with
77
83
  * `/proj/` where the watcher used `/proj` would otherwise publish a SECOND
78
84
  * bell for the same record, and neither pass would ever clear the other's. */
79
- function completionLegacyId(slug, itemId, root) {
80
- return `${LEGACY_ID_PREFIX}${root === void 0 ? "" : `${ROOT_MARK}${require_root.canonicalRoot(root)}${ROOT_SEP}`}${slug}:${itemId}`;
85
+ /** Exported for the format pin in `test/collection-watchers/test_completionId.ts`.
86
+ * Deliberately NOT on the package barrel: the id is a disk format, not an API. */
87
+ function completionLegacyId(slug, itemId, root, aid) {
88
+ if (root !== void 0 && aid !== void 0) throw new Error(`completionLegacyId: "${slug}" carries both a root (${root}) and an app (${aid})`);
89
+ if (!require_calendarGrid.isValidCollectionName(slug)) throw new Error(`completionLegacyId: collection name "${slug}" is not a valid collection name`);
90
+ if (aid !== void 0 && !require_calendarGrid.isValidCollectionName(aid)) throw new Error(`completionLegacyId: app id "${aid}" is not a valid collection name`);
91
+ return `${LEGACY_ID_PREFIX}${aid !== void 0 ? `${SHARED_MARK}${aid}${ROOT_SEP}` : root === void 0 ? "" : `${ROOT_MARK}${require_root.canonicalRoot(root)}${ROOT_SEP}`}${slug}:${itemId}`;
81
92
  }
82
93
  /** Decode a key back into its (root, slug, itemId), or null if the string
83
94
  * didn't originate from this module. Accepts both the rooted and the
84
95
  * original root-less form — see the compatibility rule above. Used by the
85
96
  * sweep step. */
97
+ /** @see completionLegacyId — exported for the same reason. */
86
98
  function parseCompletionLegacyId(legacyId) {
87
99
  if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;
88
100
  let body = legacyId.slice(22);
89
101
  let root;
90
- if (body.startsWith(ROOT_MARK)) {
102
+ let aid;
103
+ if (body.startsWith(ROOT_MARK) || body.startsWith(SHARED_MARK)) {
104
+ const shared = body.startsWith(SHARED_MARK);
91
105
  const sep = body.indexOf(ROOT_SEP);
92
106
  if (sep < 0) return null;
93
- root = body.slice(1, sep);
107
+ const scope = body.slice(1, sep);
108
+ if (shared) aid = scope;
109
+ else root = require_root.canonicalRoot(scope);
94
110
  body = body.slice(sep + 1);
95
111
  }
96
112
  const colon = body.indexOf(":");
@@ -99,6 +115,10 @@ function parseCompletionLegacyId(legacyId) {
99
115
  slug: body.slice(0, colon),
100
116
  itemId: body.slice(colon + 1)
101
117
  };
118
+ if (aid !== void 0) return {
119
+ aid,
120
+ ...parsed
121
+ };
102
122
  return root === void 0 ? parsed : {
103
123
  root,
104
124
  ...parsed
@@ -113,16 +133,28 @@ function resolveDisplayLabel(schema, item, itemId) {
113
133
  const label = require_calendarGrid.fieldText(item[displayField]).trim();
114
134
  return label.length > 0 ? label : itemId;
115
135
  }
136
+ /** The scope a collection's bells belong to.
137
+ *
138
+ * A shared collection's `appId` wins over the call's `workspaceRoot`: the
139
+ * root is where this host happens to have the repository checked out, which
140
+ * is exactly the fact that must NOT reach the identity. A local collection is
141
+ * unchanged — its root is present only when the call carried an explicit one,
142
+ * so a single-workspace host's ids stay byte-identical to what it has already
143
+ * written to `active.json`. */
144
+ function bellScopeOf(collection, ioOpts) {
145
+ return collection.appId === void 0 ? { root: ioOpts.workspaceRoot } : { aid: collection.appId };
146
+ }
147
+ var scopedLegacyId = (slug, itemId, scope) => completionLegacyId(slug, itemId, scope.root, scope.aid);
116
148
  /** Every active bell entry whose key matches this (slug, itemId).
117
149
  * Returns multiple when defensive cleanup is needed. Scans `listAll()`
118
150
  * — cheap because the active set is bounded. */
119
- async function findActiveEntries(slug, itemId, root) {
151
+ async function findActiveEntries(slug, itemId, scope) {
120
152
  const adapter = requireAdapter();
121
- const legacyId = completionLegacyId(slug, itemId, root);
153
+ const legacyId = scopedLegacyId(slug, itemId, scope);
122
154
  return (await require_notifier.listAll()).filter((entry) => adapter.readEntry(entry.pluginData)?.legacyId === legacyId);
123
155
  }
124
- async function findActiveEntryIds(slug, itemId, root) {
125
- return (await findActiveEntries(slug, itemId, root)).map((entry) => entry.id);
156
+ async function findActiveEntryIds(slug, itemId, scope) {
157
+ return (await findActiveEntries(slug, itemId, scope)).map((entry) => entry.id);
126
158
  }
127
159
  var ensureLocks = /* @__PURE__ */ new Map();
128
160
  /** Bell priority for a record: the FIRST flagged value in `notifyWhen.in`
@@ -135,8 +167,8 @@ function notifyPriorityForItem(schema, item) {
135
167
  const value = item[spec.field] === void 0 || item[spec.field] === null ? "" : String(item[spec.field]);
136
168
  return spec.in.indexOf(value) === 0 ? "high" : "normal";
137
169
  }
138
- async function ensureItemNotification(slug, schema, itemId, displayLabel, priority, root) {
139
- const legacyId = completionLegacyId(slug, itemId, root);
170
+ async function ensureItemNotification(slug, schema, itemId, displayLabel, priority, scope) {
171
+ const legacyId = scopedLegacyId(slug, itemId, scope);
140
172
  while (true) {
141
173
  const inflight = ensureLocks.get(legacyId);
142
174
  if (!inflight) break;
@@ -149,7 +181,7 @@ async function ensureItemNotification(slug, schema, itemId, displayLabel, priori
149
181
  legacyId,
150
182
  displayLabel,
151
183
  priority,
152
- root
184
+ scope
153
185
  }) };
154
186
  ensureLocks.set(legacyId, lock);
155
187
  try {
@@ -162,8 +194,9 @@ async function ensureItemNotification(slug, schema, itemId, displayLabel, priori
162
194
  * place (preserving id / position / createdAt) so a record whose flagged
163
195
  * value changed while it stayed pending re-colours the bell without a
164
196
  * clear+republish flicker. No-op when the stored priority already matches. */
165
- async function reconcileEntrySeverity(slug, itemId, entries, priority, root) {
197
+ async function reconcileEntrySeverity(slug, itemId, entries, priority, scope) {
166
198
  const adapter = requireAdapter();
199
+ const { root } = scope;
167
200
  for (const entry of entries) {
168
201
  const parsed = adapter.readEntry(entry.pluginData);
169
202
  if (!parsed || parsed.priority === priority) continue;
@@ -180,12 +213,13 @@ async function reconcileEntrySeverity(slug, itemId, entries, priority, root) {
180
213
  });
181
214
  }
182
215
  }
183
- async function doEnsureItemNotification({ slug, schema, itemId, legacyId, displayLabel, priority, root }) {
216
+ async function doEnsureItemNotification({ slug, schema, itemId, legacyId, displayLabel, priority, scope }) {
184
217
  const adapter = requireAdapter();
218
+ const { root } = scope;
185
219
  try {
186
- const existing = await findActiveEntries(slug, itemId, root);
220
+ const existing = await findActiveEntries(slug, itemId, scope);
187
221
  if (existing.length > 0) {
188
- await reconcileEntrySeverity(slug, itemId, existing, priority, root);
222
+ await reconcileEntrySeverity(slug, itemId, existing, priority, scope);
189
223
  return;
190
224
  }
191
225
  const navigateTarget = adapter.buildNavigateTarget(slug, itemId, root);
@@ -215,9 +249,18 @@ async function doEnsureItemNotification({ slug, schema, itemId, legacyId, displa
215
249
  /** Idempotently clear EVERY bell entry that matches this (slug, itemId).
216
250
  * Silent no-op when nothing matches. The "every" is defensive: if a
217
251
  * duplicate ever slips through, this drains the lot. */
218
- async function clearItemNotification(slug, itemId, root) {
252
+ async function clearItemNotification(slug, itemId, root, aid) {
253
+ await clearScoped(slug, itemId, {
254
+ root,
255
+ aid
256
+ });
257
+ }
258
+ /** The scoped form every internal caller uses. `clearItemNotification` keeps
259
+ * its positional `(slug, itemId, root?)` shape because it is public API
260
+ * (MulmoTerminal calls it); `aid` is additive. */
261
+ async function clearScoped(slug, itemId, scope) {
219
262
  try {
220
- const ids = await findActiveEntryIds(slug, itemId, root);
263
+ const ids = await findActiveEntryIds(slug, itemId, scope);
221
264
  for (const entryId of ids) await require_notifier.clear(entryId);
222
265
  } catch (err) {
223
266
  log().warn("notify clear failed", {
@@ -237,19 +280,19 @@ async function clearItemNotification(slug, itemId, root) {
237
280
  * `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */
238
281
  async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
239
282
  const { slug, schema } = collection;
240
- const root = ioOpts.workspaceRoot;
283
+ const scope = bellScopeOf(collection, ioOpts);
241
284
  if (!schema.completionField) {
242
- await clearItemNotification(slug, itemId, root);
285
+ await clearScoped(slug, itemId, scope);
243
286
  return;
244
287
  }
245
288
  const item = await require_discovery.storeFor(collection, ioOpts).read(itemId);
246
289
  if (item === null) {
247
- await clearItemNotification(slug, itemId, root);
290
+ await clearScoped(slug, itemId, scope);
248
291
  return;
249
292
  }
250
293
  await require_server.maybeSpawnSuccessor(collection, item, itemId, ioOpts);
251
294
  if (require_promptSafety.itemIsDone(schema, item)) {
252
- await clearItemNotification(slug, itemId, root);
295
+ await clearScoped(slug, itemId, scope);
253
296
  return;
254
297
  }
255
298
  if (schema.triggerField) {
@@ -261,15 +304,15 @@ async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
261
304
  triggerField: schema.triggerField
262
305
  });
263
306
  if (due !== true) {
264
- await clearItemNotification(slug, itemId, root);
307
+ await clearScoped(slug, itemId, scope);
265
308
  return;
266
309
  }
267
310
  }
268
311
  if (!require_promptSafety.whenMatches(schema.notifyWhen, item)) {
269
- await clearItemNotification(slug, itemId, root);
312
+ await clearScoped(slug, itemId, scope);
270
313
  return;
271
314
  }
272
- await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item), root);
315
+ await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item), scope);
273
316
  }
274
317
  /** Boot-time reconcile: walk every record of the collection once (through
275
318
  * its store) and reconcile it. Catches up changes that happened while the
@@ -314,9 +357,10 @@ async function reconcileAllItems(collection, ioOpts = {}, now = evalNow()) {
314
357
  * on produces a root-less id, so skipping it would strand a bell no pass can
315
358
  * ever clear, while the reconcile publishes a second rooted one beside it.
316
359
  * Clearing converges — the record republishes rooted if still pending. */
317
- function sweepVerdict(entryRoot, ownRoot) {
318
- if (entryRoot === ownRoot) return "mine";
319
- if (entryRoot === void 0) return "drop-legacy";
360
+ function sweepVerdict(entry, ownRoot) {
361
+ if (entry.aid !== void 0) return "shared";
362
+ if (entry.root === ownRoot) return "mine";
363
+ if (entry.root === void 0) return "drop-legacy";
320
364
  return "skip";
321
365
  }
322
366
  /** Should this entry's bell be dropped? True when the collection is gone, no
@@ -324,12 +368,40 @@ function sweepVerdict(entryRoot, ownRoot) {
324
368
  * `notifyWhen`. The reverse of the reconcile invariant, applied to an entry
325
369
  * whose record the forward pass can never walk to (a delete leaves a bell that
326
370
  * a walk over the SURVIVING records cannot clear). */
327
- async function isStaleEntry(slug, itemId, opts) {
371
+ async function isStaleEntry(slug, itemId, opts, aid) {
328
372
  const collection = await require_discovery.loadCollection(slug, opts);
373
+ if (aid !== void 0 && collection?.appId !== aid) return false;
329
374
  if (!collection || !collection.schema.completionField) return true;
375
+ return recordNoLongerBells(collection, itemId, opts);
376
+ }
377
+ /** The record half of the staleness question: gone, done, or outside
378
+ * `notifyWhen`. Split out so the caller stays one decision — WHOSE entry this
379
+ * is — and this one stays the other. */
380
+ async function recordNoLongerBells(collection, itemId, opts) {
330
381
  const item = await require_discovery.storeFor(collection, opts).read(itemId);
331
382
  return item === null || require_promptSafety.itemIsDone(collection.schema, item) || !require_promptSafety.whenMatches(collection.schema.notifyWhen, item);
332
383
  }
384
+ /** One entry's fate: whose it is (`sweepVerdict`), then — when it is this
385
+ * sweep's to judge — whether its record still justifies the bell. Extracted so
386
+ * the loop above stays a walk and this stays the decision. */
387
+ async function sweepOneEntry(entryId, parsed, ownRoot, opts) {
388
+ const verdict = sweepVerdict(parsed, ownRoot);
389
+ if (verdict === "skip") return;
390
+ if (verdict === "drop-legacy") {
391
+ await require_notifier.clear(entryId);
392
+ return;
393
+ }
394
+ const { slug, itemId } = parsed;
395
+ try {
396
+ if (await isStaleEntry(slug, itemId, opts, verdict === "shared" ? parsed.aid : void 0)) await require_notifier.clear(entryId);
397
+ } catch (err) {
398
+ log().warn("sweep entry failed", {
399
+ slug,
400
+ itemId,
401
+ error: errMsg(err)
402
+ });
403
+ }
404
+ }
333
405
  async function sweepStaleActiveEntries(opts = {}) {
334
406
  const adapter = requireAdapter();
335
407
  let entries;
@@ -345,22 +417,7 @@ async function sweepStaleActiveEntries(opts = {}) {
345
417
  if (!own) continue;
346
418
  const parsed = parseCompletionLegacyId(own.legacyId);
347
419
  if (!parsed) continue;
348
- const verdict = sweepVerdict(parsed.root, ownRoot);
349
- if (verdict === "skip") continue;
350
- if (verdict === "drop-legacy") {
351
- await require_notifier.clear(entry.id);
352
- continue;
353
- }
354
- const { slug, itemId } = parsed;
355
- try {
356
- if (await isStaleEntry(slug, itemId, opts)) await require_notifier.clear(entry.id);
357
- } catch (err) {
358
- log().warn("sweep entry failed", {
359
- slug,
360
- itemId,
361
- error: errMsg(err)
362
- });
363
- }
420
+ await sweepOneEntry(entry.id, parsed, ownRoot, opts);
364
421
  }
365
422
  }
366
423
  /** Test-only: clear the per-key in-flight locks. */
@@ -570,9 +627,58 @@ async function _tickTimeTriggersForTesting(now, workspaceRoot) {
570
627
  async function tickTimeTriggers(gen, now = evalNow()) {
571
628
  for (const entry of gen.watchers.values()) {
572
629
  const { schema } = entry.collection;
630
+ if (cannotReportChanges(gen, entry.collection)) continue;
573
631
  if (!schema.triggerField && !schema.spawn) continue;
574
632
  await reconcileAllItems(entry.collection, gen.discoveryOpts, now);
575
633
  }
634
+ await tickUnwatchedCollections(gen, now);
635
+ }
636
+ /** True when the collection's store implements no `watch` — nothing will ever
637
+ * tell this module its records moved, so the clock tick is its only change
638
+ * detection. A capability question, deliberately not a backend one: the day a
639
+ * shared collection's store grows an `onSnapshot` watch, it stops being
640
+ * special here with no edit to this file. */
641
+ function cannotReportChanges(gen, collection) {
642
+ return require_discovery.storeFor(collection, gen.discoveryOpts).watch === void 0;
643
+ }
644
+ /** True when a schema declares behaviour that only a reconcile pass can
645
+ * produce: bells (`completionField`), date-triggered bells (`triggerField`),
646
+ * or recurrence successors (`spawn`). */
647
+ function needsReconcilePass(schema) {
648
+ return Boolean(schema.completionField ?? schema.triggerField ?? schema.spawn);
649
+ }
650
+ /** One unwatched collection's reconcile pass. Extracted so the single-flight
651
+ * callback doesn't close over loop state.
652
+ *
653
+ * `reconcileAllItems` already swallows a failing store read (it logs and
654
+ * returns), so a closed session or a denied rule surfaces there, not as a
655
+ * rejection here. This catch is only for the unexpected — it must not let one
656
+ * collection's fault abort the rest of the tick. */
657
+ async function reconcileUnwatched(gen, collection, now) {
658
+ try {
659
+ await runSingleFlight(gen.collectionSlots, collection.slug, () => reconcileAllItems(collection, gen.discoveryOpts, now));
660
+ } catch (err) {
661
+ log().warn("unwatched collection reconcile failed", {
662
+ slug: collection.slug,
663
+ error: errMsg(err)
664
+ });
665
+ }
666
+ }
667
+ /** Stand in for the store-change path (2) on backends that don't have one.
668
+ *
669
+ * Gated on `needsReconcilePass` so a collection declaring none of it costs
670
+ * nothing: unlike the local backends, every pass here is a network round trip.
671
+ *
672
+ * A collection that DROPS OUT of eligibility (its `completionField` edited
673
+ * away, or the collection removed) is not this pass's problem — `syncWatchers`
674
+ * already sweeps on a changed schema and on a vanished slug, and the sweep is
675
+ * what clears bells the declaration no longer justifies. */
676
+ async function tickUnwatchedCollections(gen, now) {
677
+ if (require_discovery.firestoreHandle() === null) return;
678
+ const pending = [...gen.watchers.values()].map((entry) => entry.collection).filter((collection) => cannotReportChanges(gen, collection) && needsReconcilePass(collection.schema));
679
+ if (pending.length === 0) return;
680
+ for (const collection of pending) await reconcileUnwatched(gen, collection, now);
681
+ await sweepStaleActiveEntries(gen.discoveryOpts);
576
682
  }
577
683
  /** Reconcile the watcher set against the currently-discovered
578
684
  * collections. Adds watchers for new slugs (with a boot reconcile of
@@ -613,7 +719,13 @@ function stopVanishedWatchers(gen, liveSlugs) {
613
719
  * modes. The mounted fs.watch is bound to the OLD location, so it must
614
720
  * be remounted, not just re-reconciled. */
615
721
  function storagePathChanged(previous, next) {
616
- return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.path !== next.storage?.path;
722
+ return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.type !== next.storage?.type || storageFilePath(previous.storage) !== storageFilePath(next.storage);
723
+ }
724
+ /** The on-disk path of a storage backend, or undefined when it has none (a
725
+ * shared collection keeps its records off this filesystem, so there is no
726
+ * mount to move — only a `type` change matters for it). */
727
+ function storageFilePath(storage) {
728
+ return storage?.type === "sqlite" ? storage.path : void 0;
617
729
  }
618
730
  /** Re-reconcile already-watched collections whose schema changed since
619
731
  * the last tick. New collections fall through to `startNewWatchers`. */