@happyvertical/smrt-content 0.43.8 → 0.43.10

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 (46) hide show
  1. package/agents/content-list.md +69 -0
  2. package/dist/chunks/content-corrections-BxfA3tW1.js +107 -0
  3. package/dist/chunks/content-corrections-BxfA3tW1.js.map +1 -0
  4. package/dist/chunks/content-query-4w62u5_0.js +4092 -0
  5. package/dist/chunks/content-query-4w62u5_0.js.map +1 -0
  6. package/dist/chunks/content-reviews-BWrYEQc6.js +124 -0
  7. package/dist/chunks/content-reviews-BWrYEQc6.js.map +1 -0
  8. package/dist/chunks/content-versions-Di3vcTgb.js +284 -0
  9. package/dist/chunks/content-versions-Di3vcTgb.js.map +1 -0
  10. package/dist/chunks/contents-BV8RoP_R.js +612 -0
  11. package/dist/chunks/contents-BV8RoP_R.js.map +1 -0
  12. package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  13. package/dist/content-chat-handlers.d.ts +1 -1
  14. package/dist/content-chat-handlers.d.ts.map +1 -1
  15. package/dist/content-governance.d.ts +2 -0
  16. package/dist/content-governance.d.ts.map +1 -1
  17. package/dist/content.d.ts +2 -2
  18. package/dist/content.d.ts.map +1 -1
  19. package/dist/index.js +142 -4717
  20. package/dist/index.js.map +1 -1
  21. package/dist/manifest.json +142 -5
  22. package/dist/server/content-list-actions.d.ts +123 -0
  23. package/dist/server/content-list-actions.d.ts.map +1 -0
  24. package/dist/server.d.ts +6 -0
  25. package/dist/server.d.ts.map +1 -0
  26. package/dist/server.js +874 -0
  27. package/dist/server.js.map +1 -0
  28. package/dist/smrt-knowledge.json +96 -9
  29. package/dist/svelte/components/ContentList.svelte +1028 -24
  30. package/dist/svelte/components/ContentList.svelte.d.ts +3 -0
  31. package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
  32. package/dist/svelte/content-list-controller.d.ts +3 -1
  33. package/dist/svelte/content-list-controller.d.ts.map +1 -1
  34. package/dist/svelte/content-list-controller.js +15 -1
  35. package/dist/svelte/content-list-workflows.d.ts +87 -0
  36. package/dist/svelte/content-list-workflows.d.ts.map +1 -0
  37. package/dist/svelte/content-list-workflows.js +199 -0
  38. package/dist/svelte/i18n.contribution.d.ts +109 -0
  39. package/dist/svelte/i18n.contribution.d.ts.map +1 -1
  40. package/dist/svelte/i18n.contribution.js +109 -0
  41. package/dist/svelte/index.d.ts +1 -0
  42. package/dist/svelte/index.d.ts.map +1 -1
  43. package/dist/svelte/index.js +2 -0
  44. package/package.json +27 -22
  45. package/dist/chunks/content-transparency-Bh6axaZj.js +0 -606
  46. package/dist/chunks/content-transparency-Bh6axaZj.js.map +0 -1
package/dist/server.js ADDED
@@ -0,0 +1,874 @@
1
+ import { v as buildContentQuerySchema, w as Content, x as executeContentQuery } from "./chunks/content-query-4w62u5_0.js";
2
+ import { MAX_DATA_QUERY_PAGE_LIMIT, createDataQueryFingerprint, normalizeDataQueryRequest, startRestServer } from "@happyvertical/smrt-core";
3
+ import { createHash } from "node:crypto";
4
+ import "@happyvertical/smrt-facts";
5
+ import { PrincipalToolNotAllowedError } from "@happyvertical/smrt-agents";
6
+ import { createDataSurfaceActionAdapter, isBoundedDataSurfaceJsonValue } from "@happyvertical/smrt-agents/server";
7
+ //#region src/server/content-list-actions.ts
8
+ var CONTENT_LIST_WORKFLOW_IDS = [
9
+ "move-to-trash",
10
+ "mark-draft",
11
+ "submit-review",
12
+ "publish",
13
+ "archive",
14
+ "restore",
15
+ "automated-review",
16
+ "format-body",
17
+ "categorize",
18
+ "optimize"
19
+ ];
20
+ var ALL_SELECTION_SCOPES = [
21
+ "current-page",
22
+ "explicit-ids",
23
+ "all-matching"
24
+ ];
25
+ var NO_INPUT = {
26
+ type: "object",
27
+ additionalProperties: false
28
+ };
29
+ var OPTIONAL_REVIEW_INPUT = {
30
+ type: "object",
31
+ additionalProperties: false,
32
+ properties: {
33
+ kind: {
34
+ type: "string",
35
+ enum: [
36
+ "facts",
37
+ "safety",
38
+ "custom"
39
+ ]
40
+ },
41
+ policyKey: {
42
+ type: "string",
43
+ maxLength: 128
44
+ }
45
+ }
46
+ };
47
+ var CONTENT_UPDATE_OPERATION = {
48
+ id: "contents:update",
49
+ collection: "contents",
50
+ action: "update"
51
+ };
52
+ var CONTENT_READ_OPERATION = {
53
+ id: "contents:read",
54
+ collection: "contents",
55
+ action: "read"
56
+ };
57
+ var PUBLICATION_OPERATIONS = [
58
+ CONTENT_READ_OPERATION,
59
+ CONTENT_UPDATE_OPERATION,
60
+ ...[
61
+ {
62
+ id: "content-governance-assignments:read",
63
+ collection: "contentgovernanceassignments",
64
+ action: "read"
65
+ },
66
+ {
67
+ id: "content-governance-policies:read",
68
+ collection: "contentgovernancepolicies",
69
+ action: "read"
70
+ },
71
+ {
72
+ id: "content-governance-profiles:read",
73
+ collection: "contentgovernanceprofiles",
74
+ action: "read"
75
+ }
76
+ ],
77
+ {
78
+ id: "content-references:read",
79
+ collection: "contentreferences",
80
+ action: "read"
81
+ },
82
+ {
83
+ id: "content-assets:read",
84
+ collection: "contentassets",
85
+ action: "read"
86
+ },
87
+ {
88
+ id: "assets:read",
89
+ collection: "assets",
90
+ action: "read"
91
+ },
92
+ {
93
+ id: "facts:read",
94
+ collection: "facts",
95
+ action: "read"
96
+ },
97
+ {
98
+ id: "fact-contents:read",
99
+ collection: "factcontents",
100
+ action: "read"
101
+ },
102
+ {
103
+ id: "fact-sources:read",
104
+ collection: "factsources",
105
+ action: "read"
106
+ },
107
+ {
108
+ id: "content-reviews:read",
109
+ collection: "contentreviews",
110
+ action: "read"
111
+ },
112
+ {
113
+ id: "content-corrections:read",
114
+ collection: "contentcorrections",
115
+ action: "read"
116
+ },
117
+ {
118
+ id: "content-versions:read",
119
+ collection: "contentversions",
120
+ action: "read"
121
+ },
122
+ {
123
+ id: "content-versions:create",
124
+ collection: "contentversions",
125
+ action: "create"
126
+ }
127
+ ];
128
+ var AUTOMATED_REVIEW_OPERATIONS = [
129
+ ...PUBLICATION_OPERATIONS,
130
+ {
131
+ id: "prompt-overrides:read",
132
+ collection: "promptoverrides",
133
+ action: "read"
134
+ },
135
+ {
136
+ id: "content-reviews:create",
137
+ collection: "contentreviews",
138
+ action: "create"
139
+ }
140
+ ];
141
+ var CONTENT_LIST_WORKFLOWS = Object.freeze([
142
+ workflow("move-to-trash", "Move to trash", {
143
+ description: "Soft-delete eligible content into the trash lifecycle.",
144
+ sensitivity: "sensitive",
145
+ eligibility: ["content is not already deleted"],
146
+ consequences: ["Content leaves active views and becomes restorable from trash."]
147
+ }),
148
+ workflow("mark-draft", "Mark draft", {
149
+ description: "Return eligible active content to draft.",
150
+ eligibility: ["content is not deleted", "content is not already draft"],
151
+ consequences: ["Published content is no longer public."]
152
+ }),
153
+ workflow("submit-review", "Submit for review", {
154
+ description: "Move eligible draft content into editorial review.",
155
+ eligibility: ["content is draft"],
156
+ consequences: ["Content enters the review queue."]
157
+ }),
158
+ workflow("publish", "Publish", {
159
+ description: "Publish content that passes current governance readiness.",
160
+ sensitivity: "sensitive",
161
+ eligibility: ["content is draft, review, or archived", "publication readiness passes"],
162
+ consequences: ["Content becomes public and may create a publication snapshot."],
163
+ permissionRequirements: {
164
+ tool: "content.workflow.publish",
165
+ operations: [...PUBLICATION_OPERATIONS]
166
+ }
167
+ }),
168
+ workflow("archive", "Archive", {
169
+ description: "Remove eligible content from active publication workflows.",
170
+ eligibility: ["content is not deleted", "content is not already archived"],
171
+ consequences: ["Published content is no longer public."]
172
+ }),
173
+ workflow("restore", "Restore", {
174
+ description: "Restore deleted content to an explicit eligible status.",
175
+ inputSchema: {
176
+ type: "object",
177
+ additionalProperties: false,
178
+ required: ["status"],
179
+ properties: { status: { enum: [
180
+ "draft",
181
+ "review",
182
+ "published"
183
+ ] } }
184
+ },
185
+ eligibility: [
186
+ "content is deleted",
187
+ "requested destination is valid",
188
+ "publish readiness passes when publishing"
189
+ ],
190
+ consequences: ["Content leaves trash.", "Publishing makes content public."],
191
+ permissionRequirements: {
192
+ tool: "content.workflow.restore",
193
+ operations: [...PUBLICATION_OPERATIONS]
194
+ }
195
+ }),
196
+ workflow("automated-review", "Automated review", {
197
+ description: "Run the configured content governance review.",
198
+ execution: "background",
199
+ inputSchema: OPTIONAL_REVIEW_INPUT,
200
+ eligibility: ["content is not deleted", "content governance is configured"],
201
+ consequences: ["Creates a review result and may create a review version."],
202
+ permissionRequirements: {
203
+ tool: "content.workflow.automated-review",
204
+ operations: [...AUTOMATED_REVIEW_OPERATIONS]
205
+ }
206
+ }),
207
+ workflow("format-body", "Format body", {
208
+ description: "Run the application-owned body-formatting workflow.",
209
+ execution: "background",
210
+ inputSchema: {
211
+ type: "object",
212
+ additionalProperties: false,
213
+ properties: { format: { enum: ["markdown", "html"] } }
214
+ },
215
+ eligibility: ["content is not deleted", "a formatting handler is configured"],
216
+ consequences: ["The persisted content body may change."],
217
+ permissionRequirements: {
218
+ tool: "content.workflow.format-body",
219
+ operations: [...PUBLICATION_OPERATIONS]
220
+ }
221
+ }),
222
+ workflow("categorize", "Categorize", {
223
+ description: "Assign one required hierarchical category path.",
224
+ inputSchema: {
225
+ type: "object",
226
+ additionalProperties: false,
227
+ required: ["category"],
228
+ properties: { category: {
229
+ type: "string",
230
+ minLength: 1,
231
+ maxLength: 256
232
+ } }
233
+ },
234
+ eligibility: ["content is not deleted", "category is non-empty"],
235
+ consequences: ["The content category changes."],
236
+ permissionRequirements: {
237
+ tool: "content.workflow.categorize",
238
+ operations: [...PUBLICATION_OPERATIONS]
239
+ }
240
+ }),
241
+ workflow("optimize", "Optimize / complete", {
242
+ description: "Run the application-owned content optimization workflow.",
243
+ execution: "background",
244
+ inputSchema: {
245
+ type: "object",
246
+ additionalProperties: false,
247
+ properties: { instructions: {
248
+ type: "string",
249
+ maxLength: 4096
250
+ } }
251
+ },
252
+ eligibility: ["content is draft or review", "an optimization handler is configured"],
253
+ consequences: ["Content fields may be revised or completed."]
254
+ })
255
+ ]);
256
+ function workflow(id, label, options) {
257
+ return {
258
+ id,
259
+ label,
260
+ description: options.description,
261
+ selectionScopes: options.selectionScopes ?? [...ALL_SELECTION_SCOPES],
262
+ sensitivity: options.sensitivity ?? "public",
263
+ confirmation: options.confirmation ?? "required",
264
+ execution: options.execution ?? "foreground",
265
+ inputSchema: options.inputSchema ?? NO_INPUT,
266
+ eligibility: options.eligibility,
267
+ permissionRequirements: options.permissionRequirements ?? {
268
+ tool: `content.workflow.${id}`,
269
+ operations: [CONTENT_READ_OPERATION, CONTENT_UPDATE_OPERATION]
270
+ },
271
+ consequences: options.consequences,
272
+ partialResult: {
273
+ statuses: [
274
+ "accepted",
275
+ "skipped",
276
+ "failed"
277
+ ],
278
+ preservesFailedSelection: true
279
+ }
280
+ };
281
+ }
282
+ var UnsupportedContentListWorkflowStorageError = class extends Error {
283
+ code = "UNSUPPORTED_CONTENT_LIST_WORKFLOW_STORAGE";
284
+ constructor(storage) {
285
+ super(`ContentList workflows do not support ${storage.adapterType} with immediate write-back because exported files are not transactionally rolled back. Use PostgreSQL, SQLite, ordinary DuckDB, or a non-immediate write strategy.`);
286
+ this.name = "UnsupportedContentListWorkflowStorageError";
287
+ }
288
+ };
289
+ function assertContentListWorkflowStorageSupported(storage) {
290
+ const adapterType = storage?.adapterType;
291
+ const writeStrategy = storage?.writeStrategy;
292
+ if (adapterType !== "sqlite" && adapterType !== "postgres" && adapterType !== "duckdb" && adapterType !== "json") throw new TypeError("ContentList workflow storage adapterType must be sqlite, postgres, duckdb, or json.");
293
+ if (writeStrategy !== void 0 && writeStrategy !== "immediate" && writeStrategy !== "manual" && writeStrategy !== "none") throw new TypeError("ContentList workflow storage writeStrategy must be immediate, manual, or none.");
294
+ const effectiveStrategy = adapterType === "json" ? writeStrategy ?? "immediate" : writeStrategy;
295
+ if ((adapterType === "json" || adapterType === "duckdb") && effectiveStrategy === "immediate") throw new UnsupportedContentListWorkflowStorageError({
296
+ adapterType,
297
+ writeStrategy: effectiveStrategy
298
+ });
299
+ }
300
+ var REQUIRED_QUERY_FIELDS = [
301
+ "id",
302
+ "title",
303
+ "status",
304
+ "updated_at"
305
+ ];
306
+ var DEFAULT_MAX_SELECTION_SIZE = 200;
307
+ var DEFAULT_REPRESENTATIVE_LIMIT = 5;
308
+ var ContentListActionError = class extends Error {
309
+ constructor(reason) {
310
+ super(reason);
311
+ this.reason = reason;
312
+ }
313
+ reason;
314
+ };
315
+ function isRecord(value) {
316
+ return !!value && typeof value === "object" && !Array.isArray(value);
317
+ }
318
+ function isPermissionDenied(error) {
319
+ if (!(error instanceof Error) || error.name !== "OperationPermissionError") return false;
320
+ const decision = error.decision;
321
+ return isRecord(decision) && decision.reason === "permission_denied";
322
+ }
323
+ function isWorkflowId(value) {
324
+ return CONTENT_LIST_WORKFLOW_IDS.includes(value);
325
+ }
326
+ function payloadRecord(value) {
327
+ return isRecord(value) ? value : {};
328
+ }
329
+ function validPayload(workflowId, payload) {
330
+ if (payload !== void 0 && !isRecord(payload)) return false;
331
+ const value = payloadRecord(payload);
332
+ const keys = Object.keys(value);
333
+ switch (workflowId) {
334
+ case "restore": return keys.length === 1 && typeof value.status === "string" && [
335
+ "draft",
336
+ "review",
337
+ "published"
338
+ ].includes(value.status);
339
+ case "categorize": return keys.length === 1 && typeof value.category === "string" && value.category.trim().length > 0 && value.category.length <= 256;
340
+ case "automated-review": return keys.every((key) => ["kind", "policyKey"].includes(key)) && (value.kind === void 0 || typeof value.kind === "string" && [
341
+ "facts",
342
+ "safety",
343
+ "custom"
344
+ ].includes(value.kind) && value.kind.length > 0 && value.kind.length <= 64) && (value.policyKey === void 0 || typeof value.policyKey === "string" && value.policyKey.length > 0 && value.policyKey.length <= 128);
345
+ case "format-body": return keys.every((key) => key === "format") && (value.format === void 0 || value.format === "markdown" || value.format === "html");
346
+ case "optimize": return keys.every((key) => key === "instructions") && (value.instructions === void 0 || typeof value.instructions === "string" && value.instructions.length <= 4096);
347
+ default: return keys.length === 0;
348
+ }
349
+ }
350
+ function actionResult(request, ok, reason) {
351
+ const candidate = isRecord(request) ? request : {};
352
+ const identityCandidate = isRecord(candidate.identity) ? candidate.identity : {};
353
+ const identity = typeof identityCandidate.surfaceId === "string" && [
354
+ "table",
355
+ "list",
356
+ "report",
357
+ "custom"
358
+ ].includes(String(identityCandidate.kind)) ? identityCandidate : {
359
+ surfaceId: "content-list",
360
+ kind: "table"
361
+ };
362
+ return {
363
+ version: 1,
364
+ requestId: typeof candidate.requestId === "string" ? candidate.requestId : "invalid-request",
365
+ identity,
366
+ actionId: typeof candidate.actionId === "string" ? candidate.actionId : "invalid",
367
+ phase: candidate.phase === "apply" ? "apply" : "preview",
368
+ ok,
369
+ ...reason ? { reason } : {}
370
+ };
371
+ }
372
+ function validateTarget(request) {
373
+ if (!isRecord(request) || !isWorkflowId(request.actionId) || !isRecord(request.target) || !isRecord(request.selection)) return "invalid_request";
374
+ if (Object.keys(request.target).some((key) => key !== "query" && key !== "expectedCount")) return "invalid_request";
375
+ const expectedCount = request.target.expectedCount;
376
+ if (typeof expectedCount !== "number" || !Number.isSafeInteger(expectedCount) || expectedCount < 0) return "invalid_request";
377
+ if (request.selection.scope !== "explicit-ids" && !isRecord(request.target.query)) return "invalid_request";
378
+ if (request.target.query !== void 0 && !isBoundedDataSurfaceJsonValue(request.target.query)) return "invalid_request";
379
+ }
380
+ function statusOf(content) {
381
+ return String(content.status ?? "").toLowerCase();
382
+ }
383
+ async function publishReady(content) {
384
+ const governance = await content.resolveGovernance();
385
+ if (!governance.isGoverned || !governance.enforcePublishReadiness) return true;
386
+ if (!governance.publicationProfileKey) return false;
387
+ return (await content.evaluateReviewProfile(governance.publicationProfileKey)).ready;
388
+ }
389
+ async function eligibility(workflowId, content, payload, handlers) {
390
+ const status = statusOf(content);
391
+ if (workflowId === "restore") {
392
+ if (status !== "deleted") return {
393
+ eligible: false,
394
+ reason: "not_deleted"
395
+ };
396
+ if (payloadRecord(payload).status === "published" && !await publishReady(content)) return {
397
+ eligible: false,
398
+ reason: "publish_readiness_failed"
399
+ };
400
+ return { eligible: true };
401
+ }
402
+ if (status === "deleted") return {
403
+ eligible: false,
404
+ reason: "deleted"
405
+ };
406
+ switch (workflowId) {
407
+ case "move-to-trash": return { eligible: true };
408
+ case "mark-draft": return status === "draft" ? {
409
+ eligible: false,
410
+ reason: "already_draft"
411
+ } : { eligible: true };
412
+ case "submit-review": return status === "draft" ? { eligible: true } : {
413
+ eligible: false,
414
+ reason: "requires_draft"
415
+ };
416
+ case "publish":
417
+ if (![
418
+ "draft",
419
+ "review",
420
+ "archived"
421
+ ].includes(status)) return {
422
+ eligible: false,
423
+ reason: "invalid_status"
424
+ };
425
+ return await publishReady(content) ? { eligible: true } : {
426
+ eligible: false,
427
+ reason: "publish_readiness_failed"
428
+ };
429
+ case "archive": return status === "archived" ? {
430
+ eligible: false,
431
+ reason: "already_archived"
432
+ } : { eligible: true };
433
+ case "automated-review": {
434
+ const governance = await content.resolveGovernance();
435
+ if (!governance.isGoverned) return {
436
+ eligible: false,
437
+ reason: "governance_unavailable"
438
+ };
439
+ const input = payloadRecord(payload);
440
+ const policy = typeof input.policyKey === "string" ? governance.reviewPolicies.find((entry) => entry.key === input.policyKey) : void 0;
441
+ if (typeof input.policyKey === "string" && !policy) return {
442
+ eligible: false,
443
+ reason: "review_policy_unavailable"
444
+ };
445
+ if (typeof input.kind === "string" && policy && policy.kind !== input.kind) return {
446
+ eligible: false,
447
+ reason: "review_kind_mismatch"
448
+ };
449
+ return { eligible: true };
450
+ }
451
+ case "format-body": return handlers.formatBody ? { eligible: true } : {
452
+ eligible: false,
453
+ reason: "handler_unavailable"
454
+ };
455
+ case "categorize": return { eligible: true };
456
+ case "optimize":
457
+ if (!handlers.optimize) return {
458
+ eligible: false,
459
+ reason: "handler_unavailable"
460
+ };
461
+ return ["draft", "review"].includes(status) ? { eligible: true } : {
462
+ eligible: false,
463
+ reason: "requires_draft_or_review"
464
+ };
465
+ }
466
+ }
467
+ async function applyWorkflow(workflowId, content, expectedUpdatedAt, payload, run, handlers) {
468
+ const input = payloadRecord(payload);
469
+ const save = async () => {
470
+ await content.withTransaction((bound) => bound.save({ expectedUpdatedAt }));
471
+ };
472
+ const mutationDraft = () => ({
473
+ contentId: String(content.id),
474
+ tenantId: content.tenantId,
475
+ type: content.type,
476
+ variant: content.variant,
477
+ status: content.status,
478
+ state: content.state,
479
+ fileKey: content.fileKey,
480
+ author: content.author,
481
+ name: content.name,
482
+ title: content.title,
483
+ description: content.description,
484
+ body: content.body,
485
+ bodyFormat: content.bodyFormat,
486
+ publish_date: content.publish_date,
487
+ url: content.url,
488
+ source: content.source,
489
+ original_url: content.original_url,
490
+ language: content.language,
491
+ tags: [...content.tags ?? []],
492
+ category: content.category,
493
+ metadata: structuredClone(content.metadata ?? {}),
494
+ thumbnailAssetId: content.thumbnailAssetId
495
+ });
496
+ const applyDraft = (draft) => {
497
+ content.fileKey = draft.fileKey;
498
+ content.author = draft.author;
499
+ content.name = draft.name;
500
+ content.title = draft.title;
501
+ content.description = draft.description;
502
+ content.body = draft.body;
503
+ content.bodyFormat = draft.bodyFormat;
504
+ content.publish_date = draft.publish_date;
505
+ content.url = draft.url;
506
+ content.source = draft.source;
507
+ content.original_url = draft.original_url;
508
+ content.language = draft.language;
509
+ content.tags = [...draft.tags];
510
+ content.category = draft.category;
511
+ content.metadata = structuredClone(draft.metadata);
512
+ content.thumbnailAssetId = draft.thumbnailAssetId;
513
+ };
514
+ const runHandler = async (handler) => {
515
+ const draft = mutationDraft();
516
+ await handler(draft, payload, run);
517
+ applyDraft(draft);
518
+ await save();
519
+ };
520
+ switch (workflowId) {
521
+ case "move-to-trash":
522
+ content.status = "deleted";
523
+ await save();
524
+ return;
525
+ case "mark-draft":
526
+ content.status = "draft";
527
+ await save();
528
+ return;
529
+ case "submit-review":
530
+ content.status = "review";
531
+ await save();
532
+ return;
533
+ case "publish":
534
+ content.status = "published";
535
+ await save();
536
+ return;
537
+ case "archive":
538
+ content.status = "archived";
539
+ await save();
540
+ return;
541
+ case "restore":
542
+ content.status = input.status;
543
+ await save();
544
+ return;
545
+ case "automated-review":
546
+ await content.runReviewAction({
547
+ expectedUpdatedAt,
548
+ ...typeof input.kind === "string" ? { kind: input.kind } : {},
549
+ ...typeof input.policyKey === "string" ? { policyKey: input.policyKey } : {}
550
+ });
551
+ return;
552
+ case "format-body":
553
+ if (!handlers.formatBody) throw new Error("format handler unavailable");
554
+ await runHandler(handlers.formatBody);
555
+ return;
556
+ case "categorize":
557
+ content.category = String(input.category).trim();
558
+ await save();
559
+ return;
560
+ case "optimize":
561
+ if (!handlers.optimize) throw new Error("optimize handler unavailable");
562
+ await runHandler(handlers.optimize);
563
+ return;
564
+ }
565
+ }
566
+ function revisionFingerprint(rows) {
567
+ const canonical = rows.map((row) => `${String(row.id)}\0${String(row.updated_at ?? "")}`).sort();
568
+ return createHash("sha256").update(canonical.join("")).digest("base64url");
569
+ }
570
+ function representativeLabels(rows, limit) {
571
+ return rows.slice(0, limit).map((row) => {
572
+ return (typeof row.title === "string" ? row.title.trim() : "") || String(row.id);
573
+ });
574
+ }
575
+ async function resolveQuerySelection(request, collection, scope, maxSelectionSize, representativeLimit, revision) {
576
+ const schema = await buildContentQuerySchema();
577
+ const selection = request.selection;
578
+ const rows = [];
579
+ let queryFingerprint;
580
+ if (selection.scope === "explicit-ids") {
581
+ const ids = [...new Set(selection.rowIds.map((rowId) => String(rowId)))];
582
+ if (ids.length > maxSelectionSize) throw new ContentListActionError("limit_exceeded");
583
+ queryFingerprint = `explicit:${revisionFingerprint(ids.map((id) => ({ id })))}`;
584
+ for (let start = 0; start < ids.length; start += 100) {
585
+ const chunk = ids.slice(start, start + 100);
586
+ const result = await executeContentQuery(collection, {
587
+ version: 1,
588
+ requestId: `content-action-explicit-${start}`,
589
+ mode: "rows",
590
+ projection: [...REQUIRED_QUERY_FIELDS],
591
+ filter: {
592
+ kind: "condition",
593
+ field: "id",
594
+ operator: "in",
595
+ value: chunk
596
+ },
597
+ sort: [{
598
+ field: "id",
599
+ direction: "asc"
600
+ }],
601
+ page: {
602
+ kind: "offset",
603
+ offset: 0,
604
+ limit: Math.max(1, chunk.length)
605
+ }
606
+ }, { scope });
607
+ rows.push(...result.rows);
608
+ }
609
+ queryFingerprint = `${queryFingerprint}:${revisionFingerprint(rows)}`;
610
+ } else {
611
+ const input = request.target.query;
612
+ if (!input) throw new ContentListActionError("invalid_target");
613
+ const normalized = normalizeDataQueryRequest(input, schema);
614
+ const allMatchingPageLimit = Math.min(maxSelectionSize, MAX_DATA_QUERY_PAGE_LIMIT);
615
+ const querySchema = selection.scope === "all-matching" ? {
616
+ ...schema,
617
+ defaultPageLimit: Math.min(schema.defaultPageLimit ?? 200, allMatchingPageLimit),
618
+ maxPageLimit: allMatchingPageLimit
619
+ } : schema;
620
+ if (normalized.mode !== "rows") throw new ContentListActionError("invalid_target");
621
+ for (const field of REQUIRED_QUERY_FIELDS) if (!normalized.projection?.includes(field)) throw new ContentListActionError("missing_action_projection");
622
+ const baseFingerprint = createDataQueryFingerprint(normalized, schema);
623
+ if (selection.scope === "all-matching" && selection.queryFingerprint !== baseFingerprint) throw new ContentListActionError("stale_query_fingerprint");
624
+ const pageLimit = selection.scope === "current-page" ? normalized.page?.limit ?? 200 : allMatchingPageLimit;
625
+ const offset = selection.scope === "current-page" && normalized.page?.kind === "offset" ? normalized.page.offset : 0;
626
+ const result = await executeContentQuery(collection, {
627
+ ...normalized,
628
+ requestId: `${normalized.requestId}-action-${offset}`.slice(0, 128),
629
+ page: {
630
+ kind: "offset",
631
+ offset,
632
+ limit: Math.max(1, pageLimit)
633
+ }
634
+ }, {
635
+ scope,
636
+ schema: querySchema
637
+ });
638
+ if (result.total.kind !== "exact") throw new ContentListActionError("count_unavailable");
639
+ if (selection.scope === "all-matching" && result.total.value > pageLimit) throw new ContentListActionError("limit_exceeded");
640
+ rows.push(...result.rows);
641
+ if (selection.scope === "all-matching" && rows.length !== result.total.value) throw new ContentListActionError("matching_count_drifted");
642
+ if (selection.scope === "all-matching") {
643
+ if (new Set(rows.map((row) => String(row.id))).size !== rows.length) throw new ContentListActionError("matching_count_drifted");
644
+ }
645
+ queryFingerprint = `${baseFingerprint}:${revisionFingerprint(rows)}`;
646
+ }
647
+ if (request.target.expectedCount !== rows.length) throw new ContentListActionError("matching_count_drifted");
648
+ return {
649
+ revision,
650
+ queryFingerprint,
651
+ rowIds: rows.map((row) => row.id),
652
+ rows,
653
+ representativeLabels: representativeLabels(rows, representativeLimit),
654
+ scope: selection.scope
655
+ };
656
+ }
657
+ function defaultDescriptor(maxSelectionSize) {
658
+ return {
659
+ version: 1,
660
+ identity: {
661
+ surfaceId: "content-list",
662
+ kind: "table"
663
+ },
664
+ schemaVersion: 2,
665
+ label: "Contents",
666
+ rowKey: "id",
667
+ columns: [
668
+ {
669
+ id: "id",
670
+ label: "Content id",
671
+ fieldName: "id",
672
+ role: "row-key",
673
+ capabilities: ["read", "project"]
674
+ },
675
+ {
676
+ id: "title",
677
+ label: "Title",
678
+ fieldName: "title",
679
+ capabilities: ["read", "project"]
680
+ },
681
+ {
682
+ id: "status",
683
+ label: "Status",
684
+ fieldName: "status",
685
+ role: "status",
686
+ capabilities: ["read", "project"]
687
+ }
688
+ ],
689
+ query: {
690
+ modes: ["rows", "count"],
691
+ projectableColumnIds: [
692
+ "id",
693
+ "title",
694
+ "status"
695
+ ]
696
+ },
697
+ controls: [],
698
+ actions: CONTENT_LIST_WORKFLOWS.map((entry) => ({
699
+ id: entry.id,
700
+ label: entry.label,
701
+ description: entry.description,
702
+ sensitivity: entry.sensitivity,
703
+ selectionScopes: [...entry.selectionScopes],
704
+ requiresConfirmation: entry.confirmation === "required"
705
+ })),
706
+ limits: {
707
+ maxQueryRows: 200,
708
+ maxQueryBytes: 1e6,
709
+ maxSelectionSize
710
+ }
711
+ };
712
+ }
713
+ function createContentListActionAdapter(options) {
714
+ assertContentListWorkflowStorageSupported(options.workflowStorage);
715
+ const handlers = options.handlers ?? {};
716
+ const maxSelectionSize = options.maxSelectionSize ?? options.descriptor?.limits.maxSelectionSize ?? DEFAULT_MAX_SELECTION_SIZE;
717
+ if (!Number.isSafeInteger(maxSelectionSize) || maxSelectionSize < 1 || maxSelectionSize > MAX_DATA_QUERY_PAGE_LIMIT) throw new Error(`ContentList maxSelectionSize must be an integer between 1 and ${MAX_DATA_QUERY_PAGE_LIMIT} so all-matching membership resolves in one bounded query`);
718
+ const representativeLimit = options.representativeLimit ?? DEFAULT_REPRESENTATIVE_LIMIT;
719
+ const descriptor = options.descriptor ?? defaultDescriptor(maxSelectionSize);
720
+ if (descriptor.limits.maxSelectionSize !== maxSelectionSize) throw new Error("ContentList descriptor maxSelectionSize must match the server selection cap");
721
+ const resolvedByRequest = /* @__PURE__ */ new WeakMap();
722
+ const contentByRequest = /* @__PURE__ */ new WeakMap();
723
+ async function loadContent(invocation, rowId) {
724
+ const key = String(rowId);
725
+ let byRow = contentByRequest.get(invocation.request);
726
+ if (!byRow) {
727
+ byRow = /* @__PURE__ */ new Map();
728
+ contentByRequest.set(invocation.request, byRow);
729
+ }
730
+ let pending = byRow.get(key);
731
+ if (!pending) {
732
+ pending = (async () => {
733
+ const content = await (await options.collection(invocation.run)).get({ id: key });
734
+ if (!content) return null;
735
+ const expected = resolvedByRequest.get(invocation.request)?.rows.find((row) => String(row.id) === key);
736
+ const revisionValue = (value) => value instanceof Date ? value.toISOString() : String(value ?? "");
737
+ if (!expected || revisionValue(content.updated_at) !== revisionValue(expected.updated_at)) throw new ContentListActionError("row_revision_drifted");
738
+ return content;
739
+ })();
740
+ byRow.set(key, pending);
741
+ }
742
+ return pending;
743
+ }
744
+ const mapActionError = (error) => error instanceof ContentListActionError ? error.reason : isRecord(error) && error.code === "RUNTIME_REVISION_CONFLICT" ? "row_revision_drifted" : isPermissionDenied(error) || error instanceof PrincipalToolNotAllowedError ? "denied" : void 0;
745
+ const definitions = Object.fromEntries(CONTENT_LIST_WORKFLOWS.map((entry) => [entry.id, {
746
+ descriptor: {
747
+ id: entry.id,
748
+ label: entry.label,
749
+ description: entry.description,
750
+ sensitivity: entry.sensitivity,
751
+ selectionScopes: [...entry.selectionScopes],
752
+ requiresConfirmation: entry.confirmation === "required"
753
+ },
754
+ inputSchema: entry.inputSchema,
755
+ validatePayload: (payload) => validPayload(entry.id, payload) ? { valid: true } : {
756
+ valid: false,
757
+ reason: "invalid_payload"
758
+ },
759
+ confirmation: entry.confirmation,
760
+ execution: entry.execution,
761
+ tool: entry.permissionRequirements.tool,
762
+ operation: entry.permissionRequirements.operations[0] ?? CONTENT_READ_OPERATION,
763
+ authorize: async (invocation) => {
764
+ const operations = entry.id === "restore" && payloadRecord(invocation.request.payload).status !== "published" ? entry.permissionRequirements.operations.slice(0, 2) : entry.permissionRequirements.operations;
765
+ for (const operation of operations.slice(1)) try {
766
+ await invocation.run.assertOperation(operation.collection, operation.action);
767
+ } catch (error) {
768
+ if (isPermissionDenied(error)) return false;
769
+ throw error;
770
+ }
771
+ return options.authorize?.(entry.id, invocation.run) ?? true;
772
+ },
773
+ eligible: async (invocation, rowId) => {
774
+ const content = await loadContent(invocation, rowId);
775
+ if (!content) return {
776
+ eligible: false,
777
+ reason: "not_found_or_denied"
778
+ };
779
+ return eligibility(entry.id, content, invocation.request.payload, handlers);
780
+ },
781
+ apply: async (invocation, rowId) => {
782
+ const content = await loadContent(invocation, rowId);
783
+ if (!content) throw new Error("content not found");
784
+ const expectedUpdatedAt = content.updated_at;
785
+ if (!(expectedUpdatedAt instanceof Date) && typeof expectedUpdatedAt !== "string") throw new ContentListActionError("row_revision_drifted");
786
+ await applyWorkflow(entry.id, content, expectedUpdatedAt, invocation.request.payload, invocation.run, handlers);
787
+ return null;
788
+ }
789
+ }]));
790
+ const generic = createDataSurfaceActionAdapter({
791
+ state: options.state,
792
+ backgroundQueue: options.backgroundQueue,
793
+ tokenTtlMs: options.tokenTtlMs,
794
+ runAsPrincipal: options.runAsPrincipal,
795
+ resolveDeferredPrincipal: options.resolveDeferredPrincipal,
796
+ requestFingerprintExtension: (request) => request.target,
797
+ mapError: mapActionError,
798
+ resolveSurface: async (run) => ({
799
+ descriptor,
800
+ revision: await (options.revision?.(run, descriptor.identity) ?? 0),
801
+ actions: definitions
802
+ }),
803
+ resolveSelection: async (invocation, selection) => {
804
+ const request = invocation.request;
805
+ const collection = await options.collection(invocation.run);
806
+ const scope = await options.scope?.(invocation.run);
807
+ const revision = await (options.revision?.(invocation.run, descriptor.identity) ?? 0);
808
+ const resolved = await resolveQuerySelection({
809
+ ...request,
810
+ selection
811
+ }, collection, scope, maxSelectionSize, representativeLimit, revision);
812
+ resolvedByRequest.set(request, resolved);
813
+ return resolved;
814
+ }
815
+ });
816
+ async function invoke(phase, request, context) {
817
+ const invalid = validateTarget(request);
818
+ if (invalid) return actionResult(request, false, invalid);
819
+ try {
820
+ const result = await generic[phase](request, context);
821
+ if (phase !== "preview" || !result.ok) return result;
822
+ const resolved = resolvedByRequest.get(request);
823
+ const workflow2 = CONTENT_LIST_WORKFLOWS.find((entry) => entry.id === request.actionId);
824
+ return {
825
+ ...result,
826
+ details: {
827
+ ...result.details ?? {},
828
+ resolvedScope: resolved?.scope ?? request.selection.scope,
829
+ representativeLabels: resolved?.representativeLabels ?? [],
830
+ ineligible: Array.isArray(result.details?.outcomes) ? result.details.outcomes.filter((entry) => isRecord(entry) && entry.status === "skipped") : [],
831
+ consequences: workflow2?.consequences ?? [],
832
+ sensitivity: workflow2?.sensitivity ?? "public",
833
+ execution: workflow2?.execution ?? "foreground"
834
+ }
835
+ };
836
+ } catch (error) {
837
+ return actionResult(request, false, mapActionError(error) ?? "execution_failed");
838
+ } finally {
839
+ resolvedByRequest.delete(request);
840
+ }
841
+ }
842
+ return {
843
+ preview: (request, context) => invoke("preview", request, context),
844
+ apply: (request, context) => invoke("apply", request, context)
845
+ };
846
+ }
847
+ //#endregion
848
+ //#region src/server.ts
849
+ async function startServer() {
850
+ console.log("šŸš€ Starting SMRT Content Server...");
851
+ const shutdown = await startRestServer([Content], {}, {
852
+ port: 3100,
853
+ hostname: "localhost",
854
+ basePath: "/api/v1",
855
+ enableCors: true
856
+ });
857
+ console.log("āœ… Server ready!");
858
+ console.log("šŸ“” REST API: http://localhost:3100/api/v1");
859
+ console.log("šŸ“š Endpoints:");
860
+ console.log(" GET /api/v1/contents - List contents");
861
+ console.log(" POST /api/v1/contents - Create content");
862
+ console.log(" GET /api/v1/contents/:id - Get content");
863
+ console.log(" PUT /api/v1/contents/:id - Update content");
864
+ console.log(" DELETE /api/v1/contents/:id - Delete content");
865
+ console.log("\nšŸ’” Try these endpoints:");
866
+ console.log(" curl http://localhost:3100/api/v1/contents");
867
+ console.log(` curl -X POST http://localhost:3100/api/v1/contents -H "Content-Type: application/json" -d '{"title":"Test Content","body":"Test body content"}'`);
868
+ return { shutdown };
869
+ }
870
+ if (import.meta.url === `file://${process.argv[1]}`) startServer().catch(console.error);
871
+ //#endregion
872
+ export { CONTENT_LIST_WORKFLOWS, CONTENT_LIST_WORKFLOW_IDS, UnsupportedContentListWorkflowStorageError, assertContentListWorkflowStorageSupported, createContentListActionAdapter, startServer };
873
+
874
+ //# sourceMappingURL=server.js.map