@anthusai/papyrus 5.57.0 → 5.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/amplify/data/message-actions.js +68 -0
  2. package/amplify/data/message-actions.ts +70 -0
  3. package/amplify/data/resolvers/delete-own-message.js +30 -0
  4. package/amplify/data/resolvers/edit-message.js +33 -0
  5. package/amplify/data/resolvers/moderate-message.js +32 -0
  6. package/amplify/data/resolvers/post-message.js +68 -0
  7. package/amplify/data/resolvers/rename-thread.js +28 -0
  8. package/amplify/data/resource.js +28 -3
  9. package/amplify/data/resource.ts +28 -3
  10. package/amplify/data/schema.js +34 -5
  11. package/amplify/data/schema.ts +34 -5
  12. package/amplify/functions/category-action/handler.ts +51 -125
  13. package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
  14. package/components/conversation-context.tsx +29 -2
  15. package/components/conversation-provider.tsx +5 -1
  16. package/components/conversation-thread.tsx +154 -25
  17. package/components/conversations-memory-source.ts +83 -0
  18. package/components/conversations-network-source.ts +52 -0
  19. package/components/conversations-page.tsx +14 -0
  20. package/components/conversations-source.ts +36 -0
  21. package/components/conversations-view.tsx +680 -0
  22. package/components/decision-control.tsx +307 -0
  23. package/components/list-detail-layout.tsx +44 -0
  24. package/components/needs-me-bodies.tsx +18 -17
  25. package/components/needs-me-feed.tsx +16 -32
  26. package/components/news-desk-taxonomy-client.ts +216 -62
  27. package/components/newsroom-article-editor.tsx +10 -4
  28. package/components/newsroom-organizer.css +34 -1
  29. package/components/newsroom-references-view.tsx +41 -100
  30. package/components/notification-cards.tsx +9 -8
  31. package/components/notification-center.tsx +5 -7
  32. package/components/proposals-view.tsx +4 -4
  33. package/components/reference-conversation.tsx +5 -0
  34. package/components/reference-feed.tsx +129 -127
  35. package/components/reference-review-card.tsx +9 -135
  36. package/components/references-page-loading.tsx +12 -6
  37. package/components/story-reporting-trail-client.ts +4 -3
  38. package/components/story-reporting-trail.tsx +8 -0
  39. package/components/topic-steering-workspace.tsx +44 -786
  40. package/components/use-conversation.ts +36 -2
  41. package/components/use-feed-preferences.ts +33 -0
  42. package/components/use-reference-ratings.ts +11 -7
  43. package/lib/category-repository.ts +14 -38
  44. package/lib/console-chat-client.ts +8 -178
  45. package/lib/conversation-feed.ts +96 -0
  46. package/lib/conversation.ts +73 -2
  47. package/lib/cyclotron-suggestions.ts +18 -36
  48. package/lib/local-preview-seed.ts +16 -27
  49. package/lib/needs-me-items.ts +4 -4
  50. package/lib/needs-me-ranking.ts +9 -22
  51. package/lib/newsroom-board.ts +10 -1
  52. package/lib/newsroom-demo-dashboard.ts +0 -7
  53. package/lib/newsroom-organizer-nav.ts +2 -2
  54. package/lib/post-message.ts +54 -0
  55. package/lib/proposals-admin.ts +2 -2
  56. package/lib/reference-feed-keys.ts +69 -0
  57. package/lib/reference-feed-ranking.ts +77 -160
  58. package/lib/reference-policy.ts +83 -11
  59. package/lib/reference-review-details.ts +21 -57
  60. package/lib/relevance-decisions.ts +6 -2
  61. package/lib/review-drafts.ts +2 -2
  62. package/lib/review-fixture.ts +1 -1
  63. package/lib/review-requests.ts +4 -4
  64. package/lib/semantic-graph.ts +1 -1
  65. package/lib/story-reporting-trail.ts +28 -2
  66. package/package.json +12 -2
  67. package/routes.manifest.json +1 -1
  68. package/components/review-rating-grid.tsx +0 -190
@@ -0,0 +1,68 @@
1
+ import { a } from "@aws-amplify/backend";
2
+ /**
3
+ * Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
4
+ * model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
5
+ * identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
6
+ * conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
7
+ * model directly with their own grants, as before.
8
+ *
9
+ * Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
10
+ */
11
+ const people = (allow) => [allow.group("editor"), allow.group("admin")];
12
+ const adminsOnly = (allow) => [allow.group("admin")];
13
+ const onMessages = (entry) => a.handler.custom({ dataSource: a.ref("Message"), entry });
14
+ const onThreads = (entry) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
15
+ export const messageActionsSchemaDefinitions = {
16
+ postMessage: a
17
+ .mutation()
18
+ .arguments({
19
+ id: a.id().required(),
20
+ threadId: a.id().required(),
21
+ content: a.string(),
22
+ summary: a.string(),
23
+ parentMessageId: a.id(),
24
+ sequenceNumber: a.integer(),
25
+ messageKind: a.string().required(),
26
+ messageDomain: a.string().required(),
27
+ messageType: a.string(),
28
+ role: a.string(),
29
+ source: a.string(),
30
+ authorLabel: a.string(),
31
+ semanticLayer: a.string(),
32
+ searchVisibility: a.string(),
33
+ responseTarget: a.string(),
34
+ responseStatus: a.string(),
35
+ responseOwner: a.string(),
36
+ responseStartedAt: a.datetime(),
37
+ responseCompletedAt: a.datetime(),
38
+ metadata: a.json(),
39
+ newsroomFeedKey: a.string(),
40
+ })
41
+ .returns(a.ref("Message"))
42
+ .authorization(people)
43
+ .handler(onMessages("./resolvers/post-message.js")),
44
+ editMessage: a
45
+ .mutation()
46
+ .arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
47
+ .returns(a.ref("Message"))
48
+ .authorization(people)
49
+ .handler(onMessages("./resolvers/edit-message.js")),
50
+ deleteOwnMessage: a
51
+ .mutation()
52
+ .arguments({ id: a.id().required() })
53
+ .returns(a.ref("Message"))
54
+ .authorization(people)
55
+ .handler(onMessages("./resolvers/delete-own-message.js")),
56
+ moderateMessage: a
57
+ .mutation()
58
+ .arguments({ id: a.id().required() })
59
+ .returns(a.ref("Message"))
60
+ .authorization(adminsOnly)
61
+ .handler(onMessages("./resolvers/moderate-message.js")),
62
+ renameThread: a
63
+ .mutation()
64
+ .arguments({ id: a.id().required(), title: a.string().required() })
65
+ .returns(a.ref("MessageThread"))
66
+ .authorization(people)
67
+ .handler(onThreads("./resolvers/rename-thread.js")),
68
+ };
@@ -0,0 +1,70 @@
1
+ import { a } from "@aws-amplify/backend";
2
+
3
+ /**
4
+ * Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
5
+ * model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
6
+ * identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
7
+ * conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
8
+ * model directly with their own grants, as before.
9
+ *
10
+ * Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
11
+ */
12
+ const people = (allow: any) => [allow.group("editor"), allow.group("admin")];
13
+ const adminsOnly = (allow: any) => [allow.group("admin")];
14
+ const onMessages = (entry: string) => a.handler.custom({ dataSource: a.ref("Message"), entry });
15
+ const onThreads = (entry: string) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
16
+
17
+ export const messageActionsSchemaDefinitions = {
18
+ postMessage: a
19
+ .mutation()
20
+ .arguments({
21
+ id: a.id().required(),
22
+ threadId: a.id().required(),
23
+ content: a.string(),
24
+ summary: a.string(),
25
+ parentMessageId: a.id(),
26
+ sequenceNumber: a.integer(),
27
+ messageKind: a.string().required(),
28
+ messageDomain: a.string().required(),
29
+ messageType: a.string(),
30
+ role: a.string(),
31
+ source: a.string(),
32
+ authorLabel: a.string(),
33
+ semanticLayer: a.string(),
34
+ searchVisibility: a.string(),
35
+ responseTarget: a.string(),
36
+ responseStatus: a.string(),
37
+ responseOwner: a.string(),
38
+ responseStartedAt: a.datetime(),
39
+ responseCompletedAt: a.datetime(),
40
+ metadata: a.json(),
41
+ newsroomFeedKey: a.string(),
42
+ })
43
+ .returns(a.ref("Message"))
44
+ .authorization(people)
45
+ .handler(onMessages("./resolvers/post-message.js")),
46
+ editMessage: a
47
+ .mutation()
48
+ .arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
49
+ .returns(a.ref("Message"))
50
+ .authorization(people)
51
+ .handler(onMessages("./resolvers/edit-message.js")),
52
+ deleteOwnMessage: a
53
+ .mutation()
54
+ .arguments({ id: a.id().required() })
55
+ .returns(a.ref("Message"))
56
+ .authorization(people)
57
+ .handler(onMessages("./resolvers/delete-own-message.js")),
58
+ moderateMessage: a
59
+ .mutation()
60
+ .arguments({ id: a.id().required() })
61
+ .returns(a.ref("Message"))
62
+ .authorization(adminsOnly)
63
+ .handler(onMessages("./resolvers/moderate-message.js")),
64
+ renameThread: a
65
+ .mutation()
66
+ .arguments({ id: a.id().required(), title: a.string().required() })
67
+ .returns(a.ref("MessageThread"))
68
+ .authorization(people)
69
+ .handler(onThreads("./resolvers/rename-thread.js")),
70
+ };
@@ -0,0 +1,30 @@
1
+ // deleteOwnMessage: the author deletes their own Message. A soft delete: the Message stays (replies keep their place) marked
2
+ // deleted, with when and by whom. Nothing is removed.
3
+ import { util } from "@aws-appsync/utils";
4
+ import * as ddb from "@aws-appsync/utils/dynamodb";
5
+
6
+ export function request(ctx) {
7
+ const identity = ctx.identity;
8
+ if (!identity || !identity.sub) util.error("Sign in to delete a message.", "Unauthorized");
9
+ const now = util.time.nowISO8601();
10
+ return ddb.update({
11
+ key: { id: ctx.args.id },
12
+ update: {
13
+ status: ddb.operations.replace("deleted"),
14
+ deletedAt: ddb.operations.replace(now),
15
+ deletedBySub: ddb.operations.replace(identity.sub),
16
+ updatedAt: ddb.operations.replace(now),
17
+ },
18
+ condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
19
+ });
20
+ }
21
+
22
+ export function response(ctx) {
23
+ if (ctx.error) {
24
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
25
+ util.error("Only the author can delete their own message, and it may already be deleted.", "Unauthorized");
26
+ }
27
+ util.error(ctx.error.message, ctx.error.type);
28
+ }
29
+ return ctx.result;
30
+ }
@@ -0,0 +1,33 @@
1
+ // editMessage: a person changes the text of their own Message, and no one else's. The condition is checked by DynamoDB against the
2
+ // stored authorSub in the same write, so there is no window between the check and the change. A deleted Message cannot be edited.
3
+ // Authorship cannot be reassigned: the update never touches authorSub.
4
+ import { util } from "@aws-appsync/utils";
5
+ import * as ddb from "@aws-appsync/utils/dynamodb";
6
+
7
+ export function request(ctx) {
8
+ const identity = ctx.identity;
9
+ if (!identity || !identity.sub) util.error("Sign in to edit a message.", "Unauthorized");
10
+ const content = typeof ctx.args.content === "string" ? ctx.args.content.trim() : "";
11
+ if (!content) util.error("A message cannot be empty.", "BadRequest");
12
+ const now = util.time.nowISO8601();
13
+ return ddb.update({
14
+ key: { id: ctx.args.id },
15
+ update: {
16
+ content: ddb.operations.replace(content),
17
+ summary: ddb.operations.replace(ctx.args.summary ? ctx.args.summary : content.slice(0, 120)),
18
+ editedAt: ddb.operations.replace(now),
19
+ updatedAt: ddb.operations.replace(now),
20
+ },
21
+ condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
22
+ });
23
+ }
24
+
25
+ export function response(ctx) {
26
+ if (ctx.error) {
27
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
28
+ util.error("Only the author can edit a message, and a deleted message cannot be edited.", "Unauthorized");
29
+ }
30
+ util.error(ctx.error.message, ctx.error.type);
31
+ }
32
+ return ctx.result;
33
+ }
@@ -0,0 +1,32 @@
1
+ // moderateMessage: an admin removes anyone's Message. The same soft delete as the author's, with no change to the text, and only
2
+ // for the admin group (checked here as well as by the mutation's authorization).
3
+ import { util } from "@aws-appsync/utils";
4
+ import * as ddb from "@aws-appsync/utils/dynamodb";
5
+
6
+ export function request(ctx) {
7
+ const identity = ctx.identity;
8
+ if (!identity || !identity.sub) util.error("Sign in to moderate a message.", "Unauthorized");
9
+ const groups = identity.groups || [];
10
+ if (groups.indexOf("admin") < 0) util.error("Only an admin can delete someone else's message.", "Unauthorized");
11
+ const now = util.time.nowISO8601();
12
+ return ddb.update({
13
+ key: { id: ctx.args.id },
14
+ update: {
15
+ status: ddb.operations.replace("deleted"),
16
+ deletedAt: ddb.operations.replace(now),
17
+ deletedBySub: ddb.operations.replace(identity.sub),
18
+ updatedAt: ddb.operations.replace(now),
19
+ },
20
+ condition: { id: { attributeExists: true }, status: { ne: "deleted" } },
21
+ });
22
+ }
23
+
24
+ export function response(ctx) {
25
+ if (ctx.error) {
26
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
27
+ util.error("That message does not exist or is already deleted.", "NotFound");
28
+ }
29
+ util.error(ctx.error.message, ctx.error.type);
30
+ }
31
+ return ctx.result;
32
+ }
@@ -0,0 +1,68 @@
1
+ // postMessage: the only way a person's Message is created. The author is the signed-in user (the identity's sub), whatever the
2
+ // client sends: the mutation has no author argument, and a label is only a label. One DynamoDB PutItem that refuses to replace an
3
+ // existing Message (ids are derived from the sequence number, so a lost race comes back as a conflict the caller retries).
4
+ import { util } from "@aws-appsync/utils";
5
+ import * as ddb from "@aws-appsync/utils/dynamodb";
6
+
7
+ function clean(value) {
8
+ const out = {};
9
+ const keys = Object.keys(value);
10
+ for (let index = 0; index < keys.length; index++) {
11
+ const key = keys[index];
12
+ if (value[key] !== null && value[key] !== undefined) out[key] = value[key];
13
+ }
14
+ return out;
15
+ }
16
+
17
+ export function request(ctx) {
18
+ const identity = ctx.identity;
19
+ if (!identity || !identity.sub) util.error("Sign in to post a message.", "Unauthorized");
20
+ const args = ctx.args;
21
+ const content = typeof args.content === "string" ? args.content.trim() : "";
22
+ // A placeholder waiting for a responder (PENDING) may start empty; any other message needs text.
23
+ if (!content && args.responseStatus !== "PENDING") util.error("A message cannot be empty.", "BadRequest");
24
+ if (!args.id || !args.threadId) util.error("A message needs an id and a conversation.", "BadRequest");
25
+ const now = util.time.nowISO8601();
26
+ const claims = identity.claims || {};
27
+ const email = typeof claims.email === "string" ? claims.email : "";
28
+ const label = typeof args.authorLabel === "string" && args.authorLabel.trim() ? args.authorLabel.trim() : email ? email : "editor";
29
+ const item = clean({
30
+ __typename: "Message",
31
+ id: args.id,
32
+ messageKind: args.messageKind,
33
+ messageDomain: args.messageDomain,
34
+ status: "active",
35
+ summary: args.summary ? args.summary : content.slice(0, 120),
36
+ source: args.source,
37
+ authorSub: identity.sub,
38
+ authorLabel: label,
39
+ threadId: args.threadId,
40
+ parentMessageId: args.parentMessageId,
41
+ sequenceNumber: args.sequenceNumber,
42
+ role: args.role ? args.role : "human",
43
+ messageType: args.messageType,
44
+ content: content,
45
+ semanticLayer: args.semanticLayer,
46
+ searchVisibility: args.searchVisibility,
47
+ responseTarget: args.responseTarget,
48
+ responseStatus: args.responseStatus ? args.responseStatus : "COMPLETED",
49
+ responseOwner: args.responseOwner,
50
+ responseStartedAt: args.responseStartedAt,
51
+ responseCompletedAt: args.responseCompletedAt,
52
+ metadata: args.metadata,
53
+ createdAt: now,
54
+ updatedAt: now,
55
+ newsroomFeedKey: args.newsroomFeedKey,
56
+ });
57
+ return ddb.put({ key: { id: args.id }, item: item, condition: { id: { attributeExists: false } } });
58
+ }
59
+
60
+ export function response(ctx) {
61
+ if (ctx.error) {
62
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
63
+ util.error("A message with this id already exists (conditional check failed); try again.", "Conflict");
64
+ }
65
+ util.error(ctx.error.message, ctx.error.type);
66
+ }
67
+ return ctx.result;
68
+ }
@@ -0,0 +1,28 @@
1
+ // renameThread: the person who started a conversation, or an admin, gives it a new title. The browser's own update of a thread
2
+ // cannot change the title (the field is read and create only), so this is the one way. Nothing else about the thread changes.
3
+ import { util } from "@aws-appsync/utils";
4
+ import * as ddb from "@aws-appsync/utils/dynamodb";
5
+
6
+ export function request(ctx) {
7
+ const identity = ctx.identity;
8
+ if (!identity || !identity.sub) util.error("Sign in to rename a conversation.", "Unauthorized");
9
+ const title = typeof ctx.args.title === "string" ? ctx.args.title.trim() : "";
10
+ if (!title) util.error("A title cannot be empty.", "BadRequest");
11
+ const groups = identity.groups || [];
12
+ const isAdmin = groups.indexOf("admin") >= 0;
13
+ return ddb.update({
14
+ key: { id: ctx.args.id },
15
+ update: { title: ddb.operations.replace(title.slice(0, 120)) },
16
+ condition: isAdmin ? { id: { attributeExists: true } } : { createdBySub: { eq: identity.sub } },
17
+ });
18
+ }
19
+
20
+ export function response(ctx) {
21
+ if (ctx.error) {
22
+ if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
23
+ util.error("Only the person who started this conversation, or an admin, can rename it.", "Unauthorized");
24
+ }
25
+ util.error(ctx.error.message, ctx.error.type);
26
+ }
27
+ return ctx.result;
28
+ }
@@ -247,8 +247,6 @@ const schema = a.schema({
247
247
  referenceId: a.id().required(),
248
248
  status: a.string().required(),
249
249
  reasonCode: a.string(),
250
- messageId: a.id(),
251
- relationId: a.id(),
252
250
  }),
253
251
  ReferenceQualityActionResult: a.customType({
254
252
  ok: a.boolean().required(),
@@ -1569,8 +1567,26 @@ const schema = a.schema({
1569
1567
  curationStatusUpdatedAt: a.datetime(),
1570
1568
  curationStatusUpdatedBy: a.string(),
1571
1569
  curationStatusReason: a.string(),
1570
+ curationReasonCode: a.string(),
1571
+ curationActorKind: a.string(),
1572
+ curationSectionId: a.string(),
1573
+ curationShareable: a.boolean(),
1574
+ curationDelegation: a.json(),
1575
+ curationDecisionRelationId: a.string(),
1572
1576
  newsroomFeedKey: a.string(),
1573
1577
  reviewedFeedKey: a.string(),
1578
+ discoveredByMonitorId: a.string(),
1579
+ discoveryDetails: a.json(),
1580
+ discoveryRule: a.json(),
1581
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1582
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1583
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1584
+ // P(accept), written once, and never rewritten by a vote.
1585
+ feedStateKey: a.string(),
1586
+ feedArrivalAt: a.datetime(),
1587
+ predictedAcceptProbability: a.float(),
1588
+ predictionDecidedAt: a.datetime(),
1589
+ predictionSortKey: a.string(),
1574
1590
  updatedAt: a.datetime(),
1575
1591
  })
1576
1592
  .secondaryIndexes((index) => [
@@ -1583,6 +1599,9 @@ const schema = a.schema({
1583
1599
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1584
1600
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1585
1601
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1602
+ index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
1603
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1604
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1586
1605
  ])
1587
1606
  .authorization((allow) => [
1588
1607
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1670,6 +1689,9 @@ const schema = a.schema({
1670
1689
  authorSub: a.string(),
1671
1690
  authorUserProfileId: a.id(),
1672
1691
  authorLabel: a.string(),
1692
+ editedAt: a.datetime(),
1693
+ deletedAt: a.datetime(),
1694
+ deletedBySub: a.string(),
1673
1695
  threadId: a.id(),
1674
1696
  parentMessageId: a.id(),
1675
1697
  sequenceNumber: a.integer(),
@@ -1704,7 +1726,10 @@ const schema = a.schema({
1704
1726
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1705
1727
  ])
1706
1728
  .authorization((allow) => [
1707
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1729
+ // Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
1730
+ // postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
1731
+ allow.group("editor").to(["read"]),
1732
+ allow.group(adminGroup).to(["read"]),
1708
1733
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1709
1734
  ]),
1710
1735
  MessageThread: a
@@ -267,8 +267,6 @@ const schema = a.schema({
267
267
  referenceId: a.id().required(),
268
268
  status: a.string().required(),
269
269
  reasonCode: a.string(),
270
- messageId: a.id(),
271
- relationId: a.id(),
272
270
  }),
273
271
 
274
272
  ReferenceQualityActionResult: a.customType({
@@ -1682,8 +1680,26 @@ const schema = a.schema({
1682
1680
  curationStatusUpdatedAt: a.datetime(),
1683
1681
  curationStatusUpdatedBy: a.string(),
1684
1682
  curationStatusReason: a.string(),
1683
+ curationReasonCode: a.string(),
1684
+ curationActorKind: a.string(),
1685
+ curationSectionId: a.string(),
1686
+ curationShareable: a.boolean(),
1687
+ curationDelegation: a.json(),
1688
+ curationDecisionRelationId: a.string(),
1685
1689
  newsroomFeedKey: a.string(),
1686
1690
  reviewedFeedKey: a.string(),
1691
+ discoveredByMonitorId: a.string(),
1692
+ discoveryDetails: a.json(),
1693
+ discoveryRule: a.json(),
1694
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1695
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1696
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1697
+ // P(accept), written once, and never rewritten by a vote.
1698
+ feedStateKey: a.string(),
1699
+ feedArrivalAt: a.datetime(),
1700
+ predictedAcceptProbability: a.float(),
1701
+ predictionDecidedAt: a.datetime(),
1702
+ predictionSortKey: a.string(),
1687
1703
  updatedAt: a.datetime(),
1688
1704
  })
1689
1705
  .secondaryIndexes((index) => [
@@ -1696,6 +1712,9 @@ const schema = a.schema({
1696
1712
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1697
1713
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1698
1714
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1715
+ index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
1716
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1717
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1699
1718
  ])
1700
1719
  .authorization((allow) => [
1701
1720
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1786,6 +1805,9 @@ const schema = a.schema({
1786
1805
  authorSub: a.string(),
1787
1806
  authorUserProfileId: a.id(),
1788
1807
  authorLabel: a.string(),
1808
+ editedAt: a.datetime(),
1809
+ deletedAt: a.datetime(),
1810
+ deletedBySub: a.string(),
1789
1811
  threadId: a.id(),
1790
1812
  parentMessageId: a.id(),
1791
1813
  sequenceNumber: a.integer(),
@@ -1820,7 +1842,10 @@ const schema = a.schema({
1820
1842
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1821
1843
  ])
1822
1844
  .authorization((allow) => [
1823
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1845
+ // Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
1846
+ // postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
1847
+ allow.group("editor").to(["read"]),
1848
+ allow.group(adminGroup).to(["read"]),
1824
1849
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1825
1850
  ]),
1826
1851
 
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource.js";
4
4
  import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
5
5
  import { automationViewSchemaDefinitions } from "../functions/automation-view/schema.js";
6
6
  import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema.js";
7
+ import { messageActionsSchemaDefinitions } from "./message-actions.js";
7
8
  import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
8
9
  import { contentActions } from "../functions/content-actions/resource.js";
9
10
  import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
@@ -259,8 +260,6 @@ function buildBaseDefinitions(includeEditions) {
259
260
  referenceId: a.id().required(),
260
261
  status: a.string().required(),
261
262
  reasonCode: a.string(),
262
- messageId: a.id(),
263
- relationId: a.id(),
264
263
  }),
265
264
  ReferenceQualityActionResult: a.customType({
266
265
  ok: a.boolean().required(),
@@ -1475,8 +1474,26 @@ function buildBaseDefinitions(includeEditions) {
1475
1474
  curationStatusUpdatedAt: a.datetime(),
1476
1475
  curationStatusUpdatedBy: a.string(),
1477
1476
  curationStatusReason: a.string(),
1477
+ curationReasonCode: a.string(),
1478
+ curationActorKind: a.string(),
1479
+ curationSectionId: a.string(),
1480
+ curationShareable: a.boolean(),
1481
+ curationDelegation: a.json(),
1482
+ curationDecisionRelationId: a.string(),
1478
1483
  newsroomFeedKey: a.string(),
1479
1484
  reviewedFeedKey: a.string(),
1485
+ discoveredByMonitorId: a.string(),
1486
+ discoveryDetails: a.json(),
1487
+ discoveryRule: a.json(),
1488
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1489
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1490
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1491
+ // P(accept), written once, and never rewritten by a vote.
1492
+ feedStateKey: a.string(),
1493
+ feedArrivalAt: a.datetime(),
1494
+ predictedAcceptProbability: a.float(),
1495
+ predictionDecidedAt: a.datetime(),
1496
+ predictionSortKey: a.string(),
1480
1497
  updatedAt: a.datetime(),
1481
1498
  })
1482
1499
  .secondaryIndexes((index) => [
@@ -1489,6 +1506,9 @@ function buildBaseDefinitions(includeEditions) {
1489
1506
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1490
1507
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1491
1508
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1509
+ index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
1510
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1511
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1492
1512
  ])
1493
1513
  .authorization((allow) => [
1494
1514
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1573,6 +1593,9 @@ function buildBaseDefinitions(includeEditions) {
1573
1593
  authorSub: a.string(),
1574
1594
  authorUserProfileId: a.id(),
1575
1595
  authorLabel: a.string(),
1596
+ editedAt: a.datetime(),
1597
+ deletedAt: a.datetime(),
1598
+ deletedBySub: a.string(),
1576
1599
  threadId: a.id(),
1577
1600
  parentMessageId: a.id(),
1578
1601
  sequenceNumber: a.integer(),
@@ -1607,19 +1630,24 @@ function buildBaseDefinitions(includeEditions) {
1607
1630
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1608
1631
  ])
1609
1632
  .authorization((allow) => [
1610
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1633
+ // Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
1634
+ // postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
1635
+ allow.group("editor").to(["read"]),
1636
+ allow.group(adminGroup).to(["read"]),
1611
1637
  ]),
1612
1638
  MessageThread: realtimeModel({
1613
1639
  id: a.id().required(),
1614
1640
  threadKind: a.string().required(),
1615
1641
  status: a.string().required(),
1616
- title: a.string().required(),
1642
+ // The title and the creator are set when a conversation is created and read afterwards; a title changes only through
1643
+ // renameThread (its creator or an admin), and the creator never changes.
1644
+ title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
1617
1645
  summary: a.string(),
1618
1646
  primaryAnchorKind: a.string(),
1619
1647
  primaryAnchorId: a.id(),
1620
1648
  primaryAnchorLineageId: a.id(),
1621
1649
  primaryAnchorKey: a.string(),
1622
- createdBySub: a.string(),
1650
+ createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
1623
1651
  createdByUserProfileId: a.id(),
1624
1652
  createdByLabel: a.string(),
1625
1653
  messageCount: a.integer(),
@@ -2104,6 +2132,7 @@ function buildBaseDefinitions(includeEditions) {
2104
2132
  ...monitoringViewSchemaDefinitions,
2105
2133
  ...automationViewSchemaDefinitions,
2106
2134
  ...proposalActionsSchemaDefinitions,
2135
+ ...messageActionsSchemaDefinitions,
2107
2136
  };
2108
2137
  if (!includeEditions) {
2109
2138
  const mutableDefinitions = definitions;