@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.
- package/assets/helps/collection-skills.md +49 -1
- package/assets/helps/error-recovery.md +53 -0
- package/dist/{calendarGrid-Csy2rpjp.js → calendarGrid-CQ8MVSRb.js} +141 -2
- package/dist/calendarGrid-CQ8MVSRb.js.map +1 -0
- package/dist/{calendarGrid-p8K-7cuB.cjs → calendarGrid-DGILaVxI.cjs} +194 -1
- package/dist/calendarGrid-DGILaVxI.cjs.map +1 -0
- package/dist/collection/core/collectionKey.d.ts +67 -0
- package/dist/collection/core/schema.d.ts +8 -1
- package/dist/collection/core/schemaZ.d.ts +36 -26
- package/dist/collection/firestore.cjs +51 -0
- package/dist/collection/firestore.cjs.map +1 -0
- package/dist/collection/firestore.d.ts +1 -0
- package/dist/collection/firestore.js +50 -0
- package/dist/collection/firestore.js.map +1 -0
- package/dist/collection/index.cjs +11 -2
- package/dist/collection/index.d.ts +1 -0
- package/dist/collection/index.js +3 -3
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/appManifest.d.ts +53 -0
- package/dist/collection/server/delete.d.ts +10 -0
- package/dist/collection/server/discoveredCollection.d.ts +10 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/firestoreDocs.d.ts +39 -0
- package/dist/collection/server/firestoreStore.d.ts +14 -0
- package/dist/collection/server/host.d.ts +90 -8
- package/dist/collection/server/index.cjs +13 -3
- package/dist/collection/server/index.d.ts +4 -1
- package/dist/collection/server/index.js +4 -4
- package/dist/collection-watchers/index.cjs +161 -49
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +161 -49
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +36 -1
- package/dist/{discovery-D7LCN8gJ.js → discovery-CdkaURVY.js} +411 -57
- package/dist/discovery-CdkaURVY.js.map +1 -0
- package/dist/{discovery-CrA_EqUJ.cjs → discovery-Cw5xWzZD.cjs} +469 -55
- package/dist/discovery-Cw5xWzZD.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +4 -4
- package/dist/feeds/server/index.js +4 -4
- package/dist/google/index.cjs +2 -2
- package/dist/google/index.js +2 -2
- package/dist/{ingestTypes-F2IJhOLJ.js → ingestTypes-CEi-Ot7F.js} +2 -2
- package/dist/{ingestTypes-F2IJhOLJ.js.map → ingestTypes-CEi-Ot7F.js.map} +1 -1
- package/dist/{ingestTypes-CPR-h-OZ.cjs → ingestTypes-iaiq33Ph.cjs} +2 -2
- package/dist/{ingestTypes-CPR-h-OZ.cjs.map → ingestTypes-iaiq33Ph.cjs.map} +1 -1
- package/dist/{promptSafety-ACXw_2Yu.js → promptSafety-CpiME8pj.js} +2 -2
- package/dist/{promptSafety-ACXw_2Yu.js.map → promptSafety-CpiME8pj.js.map} +1 -1
- package/dist/{promptSafety-CNEW3_qS.cjs → promptSafety-NNGViiCr.cjs} +2 -2
- package/dist/{promptSafety-CNEW3_qS.cjs.map → promptSafety-NNGViiCr.cjs.map} +1 -1
- package/dist/{server-CorfPQVQ.cjs → server-9mFaGLdq.cjs} +25 -6
- package/dist/server-9mFaGLdq.cjs.map +1 -0
- package/dist/{server-D6QY853r.js → server-DdHaX_Uu.js} +25 -6
- package/dist/server-DdHaX_Uu.js.map +1 -0
- package/package.json +7 -1
- package/dist/calendarGrid-Csy2rpjp.js.map +0 -1
- package/dist/calendarGrid-p8K-7cuB.cjs.map +0 -1
- package/dist/discovery-CrA_EqUJ.cjs.map +0 -1
- package/dist/discovery-D7LCN8gJ.js.map +0 -1
- package/dist/server-CorfPQVQ.cjs.map +0 -1
- 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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
145
|
+
* two projects each owning a `tasks` collection would otherwise refresh each
|
|
146
|
+
* other's open views. */
|
|
108
147
|
root?: string;
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
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-
|
|
4
|
-
const require_discovery = require("../../discovery-
|
|
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-
|
|
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 {
|
|
3
|
-
import { A as
|
|
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-
|
|
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-
|
|
4
|
-
const require_promptSafety = require("../promptSafety-
|
|
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-
|
|
7
|
-
const require_server = require("../server-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
151
|
+
async function findActiveEntries(slug, itemId, scope) {
|
|
120
152
|
const adapter = requireAdapter();
|
|
121
|
-
const legacyId =
|
|
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,
|
|
125
|
-
return (await findActiveEntries(slug, itemId,
|
|
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,
|
|
139
|
-
const legacyId =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
220
|
+
const existing = await findActiveEntries(slug, itemId, scope);
|
|
187
221
|
if (existing.length > 0) {
|
|
188
|
-
await reconcileEntrySeverity(slug, itemId, existing, priority,
|
|
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,
|
|
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
|
|
283
|
+
const scope = bellScopeOf(collection, ioOpts);
|
|
241
284
|
if (!schema.completionField) {
|
|
242
|
-
await
|
|
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
|
|
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
|
|
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
|
|
307
|
+
await clearScoped(slug, itemId, scope);
|
|
265
308
|
return;
|
|
266
309
|
}
|
|
267
310
|
}
|
|
268
311
|
if (!require_promptSafety.whenMatches(schema.notifyWhen, item)) {
|
|
269
|
-
await
|
|
312
|
+
await clearScoped(slug, itemId, scope);
|
|
270
313
|
return;
|
|
271
314
|
}
|
|
272
|
-
await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item),
|
|
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(
|
|
318
|
-
if (
|
|
319
|
-
if (
|
|
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
|
-
|
|
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?.
|
|
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`. */
|