mdmaid-desk 0.1.29 → 0.1.31

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/README.md CHANGED
@@ -128,6 +128,27 @@ node dist/cli.js workspace add /path/to/repository \
128
128
  --repository-name "Example"
129
129
  ```
130
130
 
131
+ A canonical repository/worktree root can belong to only one workspace ID.
132
+ Ticket and task identifiers belong in document/project metadata and must not be
133
+ used as workspace IDs. If an older catalog contains two IDs for one root,
134
+ inspect a dry-run reconciliation before applying it:
135
+
136
+ ```bash
137
+ mdmaid-desk workspace reconcile task-derived-id \
138
+ --into stable-repository-id \
139
+ --json
140
+
141
+ mdmaid-desk workspace reconcile task-derived-id \
142
+ --into stable-repository-id \
143
+ --apply
144
+ ```
145
+
146
+ The operation preserves non-conflicting document and review IDs. Path
147
+ conflicts block the repair. An archived conflicting document may be discarded
148
+ only with `--discard-archived-conflicts`; review history is never discarded.
149
+ The command uses the live daemon when available, so its dry run and mutation
150
+ observe the same catalog authority.
151
+
131
152
  Register a document:
132
153
 
133
154
  ```bash
@@ -216,11 +237,11 @@ file does not remove the imported copy.
216
237
  Every registered-source render re-authorizes the real path and reconciles its
217
238
  content hash, revision, reading state, review state, and local-link mappings.
218
239
  When a foreground web service or daemon is running, mdmaid.desk also watches
219
- the parent directories of active registered sources. A committed source
220
- change, disappearance, or restoration publishes a document-scoped event: an
221
- open matching Web or TUI reader rerenders automatically, while unrelated
222
- readers only refresh queue metadata. The interfaces label registered documents
223
- as `live source` and imports as `snapshot`.
240
+ the parent directories of active registered sources. Every committed catalog
241
+ change publishes one metadata-free invalidation; Web, TUI, and waiting CLI
242
+ clients then reread authoritative state for their selected Space. The
243
+ interfaces label registered documents as `live source` and imports as
244
+ `snapshot`.
224
245
 
225
246
  Without a running service, registration and rendering still work and a manual
226
247
  render reconciles current source state. Automatic push refresh is
@@ -248,6 +269,26 @@ node dist/cli.js list --workspace example
248
269
  node dist/cli.js list --task PROJECT-123
249
270
  ```
250
271
 
272
+ Create persistent Spaces that classify current and future documents from one
273
+ or more repository, repository-namespace, workspace, or tag patterns:
274
+
275
+ ```bash
276
+ mdmaid-desk space add work --name "Work" \
277
+ --namespace github.com/example \
278
+ --tag work
279
+ mdmaid-desk space add home --name "Home" --workspace personal
280
+ mdmaid-desk space list
281
+ mdmaid-desk repository list
282
+ mdmaid-desk list --space work
283
+ ```
284
+
285
+ An agent creating a Space must ask which patterns belong to it when none were
286
+ provided. Matchers use OR semantics and membership is derived from current
287
+ repository identity and tags, so later matching documents appear automatically.
288
+ Use `space matchers set`, `space rename`, and `space delete` to manage the
289
+ definition. Space-aware commands refuse a healthy older daemon and ask for a
290
+ restart after upgrade instead of bypassing it with direct catalog access.
291
+
251
292
  Run the browser workspace as a foreground local service:
252
293
 
253
294
  ```bash
@@ -272,6 +313,10 @@ names are scoped to that token, allowing independent mdmaid.desk daemons on
272
313
  different ports to remain signed in at the same time. Stop the foreground
273
314
  service with `Ctrl-C`.
274
315
 
316
+ Project, Space, and content type are global filters. Open the Project control
317
+ and type any part of a project name to narrow its choices with a
318
+ case-insensitive substring match, then select the project normally.
319
+
275
320
  Use `--port` to select another loopback port for intentional isolated testing.
276
321
  An advanced `--public-url` option
277
322
  accepts HTTP or HTTPS `.localhost` origins; a direct HTTP origin must use the
@@ -328,15 +373,17 @@ node dist/cli.js tui
328
373
  ```
329
374
 
330
375
  The TUI reuses the running web daemon when available, so both clients share
331
- catalog events and reading state. Keys are shown in its footer; the main flow
376
+ catalog invalidations and reading state. Keys are shown in its footer; the main flow
332
377
  uses `j`/`k`, `Enter`, `/`, `m`, `u`, `a`, `b`, and `q`.
333
378
  The queue starts grouped by project; press `g` to cycle through project, tag,
334
- and one ordered-list view.
379
+ and one ordered-list view. Press `x` to cycle All and named Spaces.
335
380
  Pending review requests add `r` for the Actions view and `y`, `c`, or `x` for
336
381
  Approve, Request changes, or Reject. The response composer uses `Enter` for a
337
382
  newline, `Ctrl-D` to submit, and `Esc` to cancel.
338
383
 
339
- Press `c` from the terminal queue to enter the dedicated Change Reviews space.
384
+ Press `c` from the terminal queue to switch between Docs and Change reviews.
385
+ This content mode is independent from the Actions filter, so “Waiting for you”
386
+ can narrow either mode.
340
387
  Producers publish an implementation review and its mandatory decision with:
341
388
 
342
389
  ```bash
@@ -405,6 +452,11 @@ ${XDG_STATE_HOME:-~/.local/state}/mdmaid.desk/
405
452
  daemon.log
406
453
  ```
407
454
 
455
+ The Spaces release upgrades existing catalogs from schema 8 to schema 9.
456
+ Before the first upgraded start, follow the verified backup and downgrade
457
+ procedure in [docs/RELEASING.md](docs/RELEASING.md#schema-9-backup-and-downgrade-recovery).
458
+ In-place downgrade is not supported.
459
+
408
460
  ## Target Interaction
409
461
 
410
462
  ```mermaid
@@ -1,4 +1,5 @@
1
- import type { DocumentAction, DocumentImport, DocumentRegistration, HealthData, PublicDocument, PublicProject, PublicReviewRequest, PublicWorkspace, TerminalRenderPreferences, TerminalRender, WebRender, ReviewRequestRegistration, ReviewRequestResponse, ReviewStatus, WorkspaceRegistration } from "./api-types.js";
1
+ import type { ContentScope } from "./domain.js";
2
+ import type { BulkDocumentResult, DocumentAction, DocumentImport, DocumentRegistration, HealthData, PublicDocument, PublicProject, PublicRepository, PublicReviewRequest, PublicSpace, PublicSpaceMatcher, PublicWorkspace, PublicWorkspaceReconciliation, TerminalRenderPreferences, TerminalRender, WebRender, ReviewRequestRegistration, ReviewRequestResponse, ReviewStatus, WorkspaceRegistration, WorkspaceReconciliationRequest } from "./api-types.js";
2
3
  import type { MermaidValidationReport } from "./mermaid-validation.js";
3
4
  export declare class DeskApiError extends Error {
4
5
  readonly status: number;
@@ -6,8 +7,12 @@ export declare class DeskApiError extends Error {
6
7
  readonly validation?: MermaidValidationReport | undefined;
7
8
  constructor(status: number, code: string, message: string, validation?: MermaidValidationReport | undefined);
8
9
  }
10
+ export declare class DaemonHealthCompatibilityError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /** @deprecated Catalog invalidations now arrive as an empty object. */
9
14
  export interface CatalogEvent {
10
- action: string;
15
+ action?: string;
11
16
  documentId?: string;
12
17
  revision?: number;
13
18
  reviewRequestId?: string;
@@ -21,22 +26,44 @@ export declare class DeskApiClient {
21
26
  #private;
22
27
  constructor(baseUrl: string, token: string);
23
28
  health(signal?: AbortSignal): Promise<HealthData>;
24
- listDocuments(): Promise<PublicDocument[]>;
25
- listWorkspaces(): Promise<PublicWorkspace[]>;
26
- listProjects(): Promise<PublicProject[]>;
29
+ requireCapabilities(...required: string[]): Promise<void>;
30
+ listDocuments(scope?: ContentScope, filters?: {
31
+ archived?: boolean;
32
+ }): Promise<PublicDocument[]>;
33
+ listWorkspaces(scope?: ContentScope): Promise<PublicWorkspace[]>;
34
+ listProjects(scope?: ContentScope): Promise<PublicProject[]>;
35
+ listSpaces(): Promise<PublicSpace[]>;
36
+ getSpace(id: string): Promise<PublicSpace>;
37
+ createSpace(input: PublicSpace): Promise<PublicSpace>;
38
+ renameSpace(id: string, name: string): Promise<PublicSpace>;
39
+ replaceSpaceMatchers(id: string, matchers: PublicSpaceMatcher[]): Promise<PublicSpace>;
40
+ deleteSpace(id: string): Promise<{
41
+ id: string;
42
+ }>;
43
+ listRepositories(): Promise<PublicRepository[]>;
27
44
  addWorkspace(input: WorkspaceRegistration): Promise<PublicWorkspace>;
45
+ reconcileWorkspace(sourceWorkspaceId: string, input: WorkspaceReconciliationRequest): Promise<PublicWorkspaceReconciliation>;
28
46
  registerDocument(input: DocumentRegistration): Promise<PublicDocument>;
29
47
  importDocument(input: DocumentImport): Promise<PublicDocument>;
30
48
  listReviewRequests(filters?: {
31
49
  documentId?: string;
32
50
  status?: ReviewStatus;
33
- }): Promise<PublicReviewRequest[]>;
34
- getReviewRequest(id: string): Promise<PublicReviewRequest>;
35
- createReviewRequest(input: ReviewRequestRegistration): Promise<PublicReviewRequest>;
36
- respondToReviewRequest(id: string, input: ReviewRequestResponse): Promise<PublicReviewRequest>;
37
- renderDocument(id: string, target: "terminal", width?: number, preferences?: TerminalRenderPreferences): Promise<TerminalRender>;
38
- renderDocument(id: string, target: "web", width?: number): Promise<WebRender>;
39
- act(id: string, action: DocumentAction): Promise<PublicDocument>;
51
+ }, scope?: ContentScope): Promise<PublicReviewRequest[]>;
52
+ getReviewRequest(id: string, scope?: ContentScope): Promise<PublicReviewRequest>;
53
+ createReviewRequest(input: ReviewRequestRegistration, scope?: ContentScope): Promise<PublicReviewRequest>;
54
+ respondToReviewRequest(id: string, input: ReviewRequestResponse, scope?: ContentScope): Promise<PublicReviewRequest>;
55
+ renderDocument(id: string, target: "terminal", width?: number, preferences?: TerminalRenderPreferences, scope?: ContentScope): Promise<TerminalRender>;
56
+ renderDocument(id: string, target: "web", width?: number, preferences?: undefined, scope?: ContentScope): Promise<WebRender>;
57
+ act(id: string, action: DocumentAction, scope?: ContentScope): Promise<PublicDocument>;
58
+ bulkAct(action: "archive" | "restore", ids: string[], scope?: ContentScope): Promise<Extract<BulkDocumentResult, {
59
+ documents: PublicDocument[];
60
+ }>>;
61
+ bulkAct(action: "purge", ids: string[], scope?: ContentScope): Promise<Extract<BulkDocumentResult, {
62
+ action: "purge";
63
+ }>>;
64
+ purgeDocument(id: string, scope?: ContentScope): Promise<string>;
65
+ getDocument(id: string, scope?: ContentScope): Promise<PublicDocument>;
66
+ setDocumentTags(id: string, tags: string[], scope?: ContentScope): Promise<PublicDocument>;
40
67
  subscribeCatalog(listener: (event: CatalogEvent) => void, options?: CatalogSubscriptionOptions): Promise<void>;
41
68
  }
42
69
  export declare function isPublicReviewRequest(value: unknown): value is PublicReviewRequest;
@@ -12,6 +12,12 @@ export class DeskApiError extends Error {
12
12
  this.name = "DeskApiError";
13
13
  }
14
14
  }
15
+ export class DaemonHealthCompatibilityError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "DaemonHealthCompatibilityError";
19
+ }
20
+ }
15
21
  export class DeskApiClient {
16
22
  #baseUrl;
17
23
  #token;
@@ -34,32 +40,122 @@ export class DeskApiClient {
34
40
  authenticated: false,
35
41
  ...(signal === undefined ? {} : { signal }),
36
42
  });
37
- if (!isHealth(value)) {
43
+ if (!isCoreHealth(value)) {
38
44
  throw new Error("Daemon returned an invalid health response");
39
45
  }
40
- return value;
46
+ if (value.capabilities !== undefined &&
47
+ (!Array.isArray(value.capabilities) ||
48
+ !value.capabilities.every((capability) => typeof capability === "string"))) {
49
+ throw new DaemonHealthCompatibilityError("The running mdmaid.desk daemon has incompatible capabilities; restart it after upgrading");
50
+ }
51
+ return {
52
+ service: "mdmaid.desk",
53
+ status: "ok",
54
+ version: value.version,
55
+ ...(value.capabilities === undefined
56
+ ? {}
57
+ : { capabilities: [...new Set(value.capabilities)] }),
58
+ };
41
59
  }
42
- async listDocuments() {
43
- const value = await this.#request("/api/v1/documents");
60
+ async requireCapabilities(...required) {
61
+ const health = await this.health();
62
+ const capabilities = new Set(health.capabilities ?? []);
63
+ const missing = required.filter((capability) => !capabilities.has(capability));
64
+ if (missing.length > 0) {
65
+ throw new DaemonHealthCompatibilityError(`The running mdmaid.desk daemon does not support ${missing.join(", ")}; restart it after upgrading`);
66
+ }
67
+ }
68
+ async listDocuments(scope = {}, filters = {}) {
69
+ const query = new URLSearchParams();
70
+ if (scope.spaceId !== undefined) {
71
+ assertSpaceId(scope.spaceId);
72
+ query.set("space", scope.spaceId);
73
+ }
74
+ if (filters.archived === true) {
75
+ query.set("archived", "true");
76
+ }
77
+ const suffix = query.size === 0 ? "" : `?${query.toString()}`;
78
+ const value = await this.#request(`/api/v1/documents${suffix}`);
44
79
  if (!Array.isArray(value) || !value.every(isPublicDocument)) {
45
80
  throw new Error("Daemon returned an invalid document list");
46
81
  }
47
82
  return value;
48
83
  }
49
- async listWorkspaces() {
50
- const value = await this.#request("/api/v1/workspaces");
84
+ async listWorkspaces(scope = {}) {
85
+ const value = await this.#request(withScope("/api/v1/workspaces", scope));
51
86
  if (!Array.isArray(value) || !value.every(isPublicWorkspace)) {
52
87
  throw new Error("Daemon returned an invalid workspace list");
53
88
  }
54
89
  return value;
55
90
  }
56
- async listProjects() {
57
- const value = await this.#request("/api/v1/projects");
91
+ async listProjects(scope = {}) {
92
+ const value = await this.#request(withScope("/api/v1/projects", scope));
58
93
  if (!Array.isArray(value) || !value.every(isPublicProject)) {
59
94
  throw new Error("Daemon returned an invalid project list");
60
95
  }
61
96
  return value;
62
97
  }
98
+ async listSpaces() {
99
+ const value = await this.#request("/api/v1/spaces");
100
+ if (!Array.isArray(value) || !value.every(isPublicSpace)) {
101
+ throw new Error("Daemon returned an invalid Space list");
102
+ }
103
+ return value;
104
+ }
105
+ async getSpace(id) {
106
+ assertSpaceId(id);
107
+ const value = await this.#request(`/api/v1/spaces/${encodeURIComponent(id)}`);
108
+ if (!isPublicSpace(value)) {
109
+ throw new Error("Daemon returned an invalid Space");
110
+ }
111
+ return value;
112
+ }
113
+ async createSpace(input) {
114
+ const value = await this.#request("/api/v1/spaces", {
115
+ method: "POST",
116
+ body: input,
117
+ });
118
+ if (!isPublicSpace(value)) {
119
+ throw new Error("Daemon returned an invalid Space");
120
+ }
121
+ return value;
122
+ }
123
+ async renameSpace(id, name) {
124
+ assertSpaceId(id);
125
+ const value = await this.#request(`/api/v1/spaces/${encodeURIComponent(id)}`, {
126
+ method: "PATCH",
127
+ body: { name },
128
+ });
129
+ if (!isPublicSpace(value)) {
130
+ throw new Error("Daemon returned an invalid Space");
131
+ }
132
+ return value;
133
+ }
134
+ async replaceSpaceMatchers(id, matchers) {
135
+ assertSpaceId(id);
136
+ const value = await this.#request(`/api/v1/spaces/${encodeURIComponent(id)}/matchers`, { method: "PUT", body: { matchers } });
137
+ if (!isPublicSpace(value)) {
138
+ throw new Error("Daemon returned an invalid Space");
139
+ }
140
+ return value;
141
+ }
142
+ async deleteSpace(id) {
143
+ assertSpaceId(id);
144
+ const value = await this.#request(`/api/v1/spaces/${encodeURIComponent(id)}`, {
145
+ method: "DELETE",
146
+ });
147
+ if (!isRecord(value) || Object.keys(value).length !== 1 || value.id !== id) {
148
+ throw new Error("Daemon returned an invalid Space deletion response");
149
+ }
150
+ return { id };
151
+ }
152
+ async listRepositories() {
153
+ const value = await this.#request("/api/v1/repositories");
154
+ if (!Array.isArray(value) || !value.every(isPublicRepository)) {
155
+ throw new Error("Daemon returned an invalid repository list");
156
+ }
157
+ return value;
158
+ }
63
159
  async addWorkspace(input) {
64
160
  const value = await this.#request("/api/v1/workspaces", {
65
161
  method: "POST",
@@ -70,6 +166,14 @@ export class DeskApiClient {
70
166
  }
71
167
  return value;
72
168
  }
169
+ async reconcileWorkspace(sourceWorkspaceId, input) {
170
+ assertWorkspaceId(sourceWorkspaceId);
171
+ const value = await this.#request(`/api/v1/workspaces/${encodeURIComponent(sourceWorkspaceId)}/reconcile`, { method: "POST", body: input });
172
+ if (!isPublicWorkspaceReconciliation(value)) {
173
+ throw new Error("Daemon returned an invalid workspace reconciliation");
174
+ }
175
+ return value;
176
+ }
73
177
  async registerDocument(input) {
74
178
  const value = await this.#request("/api/v1/documents", {
75
179
  method: "POST",
@@ -90,7 +194,7 @@ export class DeskApiClient {
90
194
  }
91
195
  return value;
92
196
  }
93
- async listReviewRequests(filters = {}) {
197
+ async listReviewRequests(filters = {}, scope = {}) {
94
198
  const query = new URLSearchParams();
95
199
  if (filters.documentId !== undefined) {
96
200
  assertDocumentId(filters.documentId);
@@ -102,6 +206,7 @@ export class DeskApiClient {
102
206
  }
103
207
  query.set("status", filters.status);
104
208
  }
209
+ addScope(query, scope);
105
210
  const suffix = query.size === 0 ? "" : `?${query.toString()}`;
106
211
  const value = await this.#request(`/api/v1/review-requests${suffix}`);
107
212
  if (!Array.isArray(value) || !value.every(isPublicReviewRequest)) {
@@ -109,16 +214,16 @@ export class DeskApiClient {
109
214
  }
110
215
  return value;
111
216
  }
112
- async getReviewRequest(id) {
217
+ async getReviewRequest(id, scope = {}) {
113
218
  assertReviewRequestId(id);
114
- const value = await this.#request(`/api/v1/review-requests/${encodeURIComponent(id)}`);
219
+ const value = await this.#request(withScope(`/api/v1/review-requests/${encodeURIComponent(id)}`, scope));
115
220
  if (!isPublicReviewRequest(value)) {
116
221
  throw new Error("Daemon returned an invalid review request");
117
222
  }
118
223
  return value;
119
224
  }
120
- async createReviewRequest(input) {
121
- const value = await this.#request("/api/v1/review-requests", {
225
+ async createReviewRequest(input, scope = {}) {
226
+ const value = await this.#request(withScope("/api/v1/review-requests", scope), {
122
227
  method: "POST",
123
228
  body: input,
124
229
  });
@@ -127,15 +232,15 @@ export class DeskApiClient {
127
232
  }
128
233
  return value;
129
234
  }
130
- async respondToReviewRequest(id, input) {
235
+ async respondToReviewRequest(id, input, scope = {}) {
131
236
  assertReviewRequestId(id);
132
- const value = await this.#request(`/api/v1/review-requests/${encodeURIComponent(id)}/respond`, { method: "POST", body: input });
237
+ const value = await this.#request(withScope(`/api/v1/review-requests/${encodeURIComponent(id)}/respond`, scope), { method: "POST", body: input });
133
238
  if (!isPublicReviewRequest(value)) {
134
239
  throw new Error("Daemon returned an invalid review request");
135
240
  }
136
241
  return value;
137
242
  }
138
- async renderDocument(id, target, width = 100, preferences = {}) {
243
+ async renderDocument(id, target, width = 100, preferences = {}, scope = {}) {
139
244
  assertDocumentId(id);
140
245
  if (!Number.isSafeInteger(width) || width < 20 || width > 1_000) {
141
246
  throw new Error("render width must be an integer between 20 and 1000");
@@ -150,6 +255,7 @@ export class DeskApiClient {
150
255
  query.set("unicode", String(preferences.unicode));
151
256
  }
152
257
  }
258
+ addScope(query, scope);
153
259
  const value = await this.#request(`/api/v1/documents/${encodeURIComponent(id)}/render?${query.toString()}`);
154
260
  if (target === "terminal" && isTerminalRender(value)) {
155
261
  return value;
@@ -159,9 +265,41 @@ export class DeskApiClient {
159
265
  }
160
266
  throw new Error("Daemon returned an invalid render response");
161
267
  }
162
- async act(id, action) {
268
+ async act(id, action, scope = {}) {
163
269
  assertDocumentId(id);
164
- const value = await this.#request(`/api/v1/documents/${encodeURIComponent(id)}/${action}`, { method: "POST" });
270
+ const value = await this.#request(withScope(`/api/v1/documents/${encodeURIComponent(id)}/${action}`, scope), { method: "POST" });
271
+ if (!isPublicDocument(value)) {
272
+ throw new Error("Daemon returned an invalid document response");
273
+ }
274
+ return value;
275
+ }
276
+ async bulkAct(action, ids, scope = {}) {
277
+ assertDocumentIds(ids);
278
+ const value = await this.#request(withScope("/api/v1/documents/bulk", scope), { method: "POST", body: { action, ids } });
279
+ if (!isBulkDocumentResult(value, action)) {
280
+ throw new Error("Daemon returned an invalid bulk document response");
281
+ }
282
+ return value;
283
+ }
284
+ async purgeDocument(id, scope = {}) {
285
+ assertDocumentId(id);
286
+ const value = await this.#request(withScope(`/api/v1/documents/${encodeURIComponent(id)}/purge`, scope), { method: "POST" });
287
+ if (!isRecord(value) || value.purgedId !== id) {
288
+ throw new Error("Daemon returned an invalid purge response");
289
+ }
290
+ return value.purgedId;
291
+ }
292
+ async getDocument(id, scope = {}) {
293
+ assertDocumentId(id);
294
+ const value = await this.#request(withScope(`/api/v1/documents/${encodeURIComponent(id)}`, scope));
295
+ if (!isPublicDocument(value)) {
296
+ throw new Error("Daemon returned an invalid document response");
297
+ }
298
+ return value;
299
+ }
300
+ async setDocumentTags(id, tags, scope = {}) {
301
+ assertDocumentId(id);
302
+ const value = await this.#request(withScope(`/api/v1/documents/${encodeURIComponent(id)}/tags`, scope), { method: "PUT", body: { tags } });
165
303
  if (!isPublicDocument(value)) {
166
304
  throw new Error("Daemon returned an invalid document response");
167
305
  }
@@ -250,12 +388,39 @@ export class DeskApiClient {
250
388
  return body.data;
251
389
  }
252
390
  }
253
- function isHealth(value) {
391
+ function isCoreHealth(value) {
254
392
  return (isRecord(value) &&
255
393
  value.service === "mdmaid.desk" &&
256
394
  value.status === "ok" &&
257
395
  typeof value.version === "number");
258
396
  }
397
+ function isPublicSpaceMatcher(value) {
398
+ return isRecord(value) &&
399
+ Object.keys(value).length === 2 &&
400
+ (value.kind === "repository" ||
401
+ value.kind === "repository-namespace" ||
402
+ value.kind === "tag") &&
403
+ typeof value.value === "string";
404
+ }
405
+ function isPublicSpace(value) {
406
+ return isRecord(value) &&
407
+ Object.keys(value).length === 3 &&
408
+ typeof value.id === "string" &&
409
+ /^[a-z0-9][a-z0-9-]{0,63}$/.test(value.id) &&
410
+ typeof value.name === "string" &&
411
+ Array.isArray(value.matchers) &&
412
+ value.matchers.length > 0 &&
413
+ value.matchers.every(isPublicSpaceMatcher);
414
+ }
415
+ function isPublicRepository(value) {
416
+ return isRecord(value) &&
417
+ Object.keys(value).length === 4 &&
418
+ typeof value.key === "string" &&
419
+ typeof value.name === "string" &&
420
+ Array.isArray(value.workspaceIds) &&
421
+ value.workspaceIds.every((id) => typeof id === "string") &&
422
+ (value.kind === "remote" || value.kind === "local");
423
+ }
259
424
  function isPublicWorkspace(value) {
260
425
  return (isRecord(value) &&
261
426
  typeof value.id === "string" &&
@@ -263,6 +428,29 @@ function isPublicWorkspace(value) {
263
428
  typeof value.documentCount === "number" &&
264
429
  typeof value.route === "string");
265
430
  }
431
+ function isPublicWorkspaceReconciliation(value) {
432
+ return isRecord(value) &&
433
+ Object.keys(value).length === 7 &&
434
+ typeof value.sourceWorkspaceId === "string" &&
435
+ typeof value.targetWorkspaceId === "string" &&
436
+ typeof value.applied === "boolean" &&
437
+ Array.isArray(value.movedDocumentIds) &&
438
+ value.movedDocumentIds.every((id) => typeof id === "string") &&
439
+ Array.isArray(value.discardedDocumentIds) &&
440
+ value.discardedDocumentIds.every((id) => typeof id === "string") &&
441
+ Array.isArray(value.blockingConflicts) &&
442
+ value.blockingConflicts.every(isPublicWorkspaceReconciliationConflict) &&
443
+ typeof value.reviewRequestCount === "number" &&
444
+ Number.isSafeInteger(value.reviewRequestCount) &&
445
+ value.reviewRequestCount >= 0;
446
+ }
447
+ function isPublicWorkspaceReconciliationConflict(value) {
448
+ return isRecord(value) &&
449
+ Object.keys(value).length === 3 &&
450
+ typeof value.sourceDocumentId === "string" &&
451
+ typeof value.targetDocumentId === "string" &&
452
+ (value.reason === "path-conflict" || value.reason === "review-history");
453
+ }
266
454
  function isPublicProject(value) {
267
455
  return (isRecord(value) &&
268
456
  typeof value.id === "string" &&
@@ -303,6 +491,15 @@ function isPublicDocument(value) {
303
491
  typeof value.updatedAt === "string" &&
304
492
  typeof value.route === "string");
305
493
  }
494
+ function isBulkDocumentResult(value, action) {
495
+ if (!isRecord(value) || value.action !== action) {
496
+ return false;
497
+ }
498
+ return action === "purge"
499
+ ? Array.isArray(value.purgedIds) &&
500
+ value.purgedIds.every((id) => typeof id === "string" && isDocumentId(id))
501
+ : Array.isArray(value.documents) && value.documents.every(isPublicDocument);
502
+ }
306
503
  export function isPublicReviewRequest(value) {
307
504
  if (!isRecord(value)) {
308
505
  return false;
@@ -440,7 +637,7 @@ function dispatchEventBlock(block, listener, onReady) {
440
637
  onReady?.();
441
638
  return;
442
639
  }
443
- if (type !== "catalog") {
640
+ if (type !== "catalog-invalidated") {
444
641
  return;
445
642
  }
446
643
  let value;
@@ -450,55 +647,10 @@ function dispatchEventBlock(block, listener, onReady) {
450
647
  catch {
451
648
  throw new Error("Daemon returned an invalid catalog event");
452
649
  }
453
- const liveSourceAction = isRecord(value) &&
454
- (value.action === "source-changed" ||
455
- value.action === "source-missing" ||
456
- value.action === "source-restored");
457
- if (!isRecord(value) ||
458
- !Object.keys(value).every((key) => [
459
- "action",
460
- "documentId",
461
- "revision",
462
- "reviewRequestId",
463
- "workspaceId",
464
- ].includes(key)) ||
465
- typeof value.action !== "string" ||
466
- (value.documentId !== undefined &&
467
- (typeof value.documentId !== "string" ||
468
- !/^doc-[a-f0-9]{20}$/.test(value.documentId))) ||
469
- (value.workspaceId !== undefined &&
470
- (typeof value.workspaceId !== "string" ||
471
- !/^[a-z0-9][a-z0-9-]{0,63}$/.test(value.workspaceId))) ||
472
- (value.reviewRequestId !== undefined &&
473
- (typeof value.reviewRequestId !== "string" ||
474
- !/^review-[a-f0-9]{20}$/.test(value.reviewRequestId))) ||
475
- (value.revision !== undefined &&
476
- (typeof value.revision !== "number" ||
477
- !Number.isSafeInteger(value.revision) ||
478
- value.revision < 1)) ||
479
- (liveSourceAction &&
480
- (value.documentId === undefined || value.revision === undefined)) ||
481
- (!liveSourceAction && value.revision !== undefined) ||
482
- (value.documentId === undefined &&
483
- value.workspaceId === undefined &&
484
- value.reviewRequestId === undefined)) {
650
+ if (!isRecord(value) || Object.keys(value).length !== 0) {
485
651
  throw new Error("Daemon returned an invalid catalog event");
486
652
  }
487
- listener({
488
- action: value.action,
489
- ...(value.documentId === undefined
490
- ? {}
491
- : { documentId: value.documentId }),
492
- ...(value.revision === undefined
493
- ? {}
494
- : { revision: value.revision }),
495
- ...(value.workspaceId === undefined
496
- ? {}
497
- : { workspaceId: value.workspaceId }),
498
- ...(value.reviewRequestId === undefined
499
- ? {}
500
- : { reviewRequestId: value.reviewRequestId }),
501
- });
653
+ listener({});
502
654
  }
503
655
  function optionalString(value) {
504
656
  return value === undefined || typeof value === "string";
@@ -515,6 +667,18 @@ function nullableInteger(value) {
515
667
  function isRecord(value) {
516
668
  return typeof value === "object" && value !== null && !Array.isArray(value);
517
669
  }
670
+ function assertDocumentIds(ids) {
671
+ if (!Array.isArray(ids) ||
672
+ ids.length === 0 ||
673
+ ids.length > 500 ||
674
+ !ids.every((id) => typeof id === "string" && isDocumentId(id)) ||
675
+ new Set(ids).size !== ids.length) {
676
+ throw new Error("document ids must contain between 1 and 500 unique ids");
677
+ }
678
+ }
679
+ function isDocumentId(value) {
680
+ return /^doc-[a-f0-9]{20}$/.test(value);
681
+ }
518
682
  function assertDocumentId(id) {
519
683
  if (!/^doc-[a-f0-9]{20}$/.test(id)) {
520
684
  throw new Error("invalid document id");
@@ -525,4 +689,30 @@ function assertReviewRequestId(id) {
525
689
  throw new Error("invalid review request id");
526
690
  }
527
691
  }
692
+ function assertSpaceId(id) {
693
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(id)) {
694
+ throw new Error("invalid Space id");
695
+ }
696
+ }
697
+ function assertWorkspaceId(id) {
698
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(id)) {
699
+ throw new Error("invalid workspace id");
700
+ }
701
+ }
702
+ function addScope(query, scope) {
703
+ if (!isRecord(scope) ||
704
+ !Object.keys(scope).every((key) => key === "spaceId") ||
705
+ (scope.spaceId !== undefined && typeof scope.spaceId !== "string")) {
706
+ throw new Error("invalid content scope");
707
+ }
708
+ if (scope.spaceId !== undefined) {
709
+ assertSpaceId(scope.spaceId);
710
+ query.set("space", scope.spaceId);
711
+ }
712
+ }
713
+ function withScope(path, scope) {
714
+ const url = new URL(path, "http://mdmaid.desk.localhost");
715
+ addScope(url.searchParams, scope);
716
+ return `${url.pathname}${url.search}${url.hash}`;
717
+ }
528
718
  //# sourceMappingURL=api-client.js.map