@anthusai/papyrus 5.43.0-next.5 → 5.43.0-next.50
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/amplify/data/message-actions.js +68 -0
- package/amplify/data/message-actions.ts +70 -0
- package/amplify/data/resolvers/delete-own-message.js +30 -0
- package/amplify/data/resolvers/edit-message.js +33 -0
- package/amplify/data/resolvers/moderate-message.js +32 -0
- package/amplify/data/resolvers/post-message.js +68 -0
- package/amplify/data/resolvers/rename-thread.js +28 -0
- package/amplify/data/resource.js +34 -3
- package/amplify/data/resource.ts +35 -3
- package/amplify/data/schema.js +40 -5
- package/amplify/data/schema.ts +41 -5
- package/amplify/functions/category-action/handler.ts +53 -126
- package/amplify/functions/content-actions/handler.py +8 -0
- package/amplify/functions/content-actions/requirements.txt +2 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/monitoring-view/schema.js +16 -0
- package/amplify/functions/monitoring-view/schema.ts +16 -0
- package/amplify/functions/procedure-action/handler.ts +2 -10
- package/amplify/functions/proposal-actions/proposals.py +20 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
- package/amplify/plugins/reader-steering/function/handler.py +1 -3
- package/amplify/plugins/reader-steering/function/moderation.py +19 -0
- package/app/api/newsroom/source-frameable/route.ts +12 -0
- package/app/api/newsroom/source-icon/route.ts +20 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +2 -0
- package/app/newsroom/[section]/page.tsx +1 -0
- package/bin/papyrus-app.mjs +12 -1
- package/bin/papyrus-infra.mjs +21 -1
- package/components/auto-accepted-badge.tsx +21 -0
- package/components/conversation-context.tsx +52 -0
- package/components/conversation-provider.tsx +20 -0
- package/components/conversation-thread.tsx +188 -0
- package/components/conversations-memory-source.ts +46 -0
- package/components/conversations-network-source.ts +40 -0
- package/components/conversations-page.tsx +14 -0
- package/components/conversations-source.ts +24 -0
- package/components/conversations-view.tsx +319 -0
- package/components/decision-control.tsx +307 -0
- package/components/dev-sandbox-editor-sign-in.tsx +2 -2
- package/components/markdown-context.tsx +412 -0
- package/components/monitoring-overview-view.tsx +143 -21
- package/components/monitoring-panel.tsx +64 -4
- package/components/needs-me-bodies.tsx +302 -0
- package/components/needs-me-detail.tsx +24 -0
- package/components/needs-me-feed.tsx +594 -0
- package/components/needs-me-reference-detail.tsx +79 -0
- package/components/needs-me-shared.tsx +48 -0
- package/components/news-desk-client-provider.tsx +24 -3
- package/components/news-desk-page.tsx +2 -1
- package/components/news-desk-taxonomy-client.ts +337 -77
- package/components/newsroom-access-gate.tsx +13 -20
- package/components/newsroom-board-skeleton.tsx +20 -0
- package/components/newsroom-board-view.tsx +2 -6
- package/components/newsroom-client-shell.tsx +2 -1
- package/components/newsroom-ops-overview.tsx +88 -73
- package/components/newsroom-ops-shell.tsx +8 -28
- package/components/newsroom-organizer.css +127 -3
- package/components/newsroom-references-view.tsx +334 -158
- package/components/notification-cards.tsx +370 -0
- package/components/notification-center.tsx +376 -213
- package/components/papyrus-console-shell.tsx +2 -1
- package/components/proposals-panel.tsx +21 -24
- package/components/proposals-view.tsx +4 -2
- package/components/reader-auth-control.tsx +3 -1
- package/components/reference-capture-client.ts +138 -0
- package/components/reference-capture-panel.tsx +268 -0
- package/components/reference-conversation.tsx +43 -0
- package/components/reference-feed.tsx +541 -0
- package/components/reference-review-card.tsx +62 -197
- package/components/reference-source-preview.tsx +27 -5
- package/components/reference-stories.tsx +34 -0
- package/components/references-page-loading.tsx +61 -0
- package/components/references-prefetch.ts +52 -0
- package/components/story-reporting-trail-client.ts +48 -10
- package/components/story-reporting-trail.tsx +32 -13
- package/components/topic-steering-workspace.tsx +92 -1223
- package/components/ui/auto-grow-textarea.tsx +18 -0
- package/components/use-conversation.ts +84 -0
- package/components/use-media-query.ts +16 -0
- package/components/use-reference-preview-attachments.ts +101 -0
- package/components/use-reference-ratings.ts +192 -0
- package/infra/amplify-app-shell.js +18 -2
- package/infra/amplify-app-shell.ts +18 -2
- package/infra/chat-stack.js +222 -0
- package/infra/chat-stack.ts +250 -0
- package/infra/chat.test.js +142 -0
- package/infra/chat.test.ts +151 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +58 -1
- package/infra/site-config.ts +78 -1
- package/lib/category-repository.ts +17 -39
- package/lib/console-chat-client.ts +8 -178
- package/lib/content-actions-client.ts +20 -0
- package/lib/conversation-feed.ts +96 -0
- package/lib/conversation.ts +154 -0
- package/lib/cyclotron-suggestions.ts +18 -36
- package/lib/local-preview-feed-fixture.ts +63 -0
- package/lib/local-preview-seed.ts +40 -29
- package/lib/local-preview-synthetic-rejects.ts +41 -0
- package/lib/monitoring-client.ts +29 -2
- package/lib/monitoring-overview.ts +52 -1
- package/lib/monitoring-paging.ts +58 -0
- package/lib/needs-me-items.ts +289 -0
- package/lib/needs-me-ranking.ts +315 -0
- package/lib/newsroom-all-pages.ts +21 -0
- package/lib/newsroom-chat-identity.ts +2 -1
- package/lib/newsroom-demo-dashboard.ts +0 -7
- package/lib/newsroom-index-filters.ts +18 -0
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/newsroom-references.ts +25 -1
- package/lib/notification-feed.ts +212 -0
- package/lib/post-message.ts +54 -0
- package/lib/proposals-client.ts +29 -0
- package/lib/reader-subject.ts +3 -2
- package/lib/reference-capture.ts +294 -0
- package/lib/reference-feed-cache.ts +170 -0
- package/lib/reference-feed-keys.ts +69 -0
- package/lib/reference-feed-ranking.ts +236 -0
- package/lib/reference-policy.ts +95 -11
- package/lib/reference-preview.ts +112 -0
- package/lib/reference-review-details.ts +21 -57
- package/lib/relevance-decisions.ts +6 -2
- package/lib/review-client.ts +71 -5
- package/lib/review-drafts.ts +277 -0
- package/lib/review-fixture.ts +56 -2
- package/lib/review-requests.ts +89 -24
- package/lib/semantic-graph.ts +1 -1
- package/lib/site-brand.ts +2 -0
- package/lib/source-frameable.ts +77 -0
- package/lib/source-icon.ts +104 -0
- package/lib/staff-groups.ts +23 -0
- package/lib/staging-gate.ts +2 -2
- package/lib/story-reporting-trail.ts +61 -4
- package/package.json +53 -2
- package/routes.manifest.json +23 -1
- package/components/newsroom-overview-skeleton.tsx +0 -25
package/amplify/data/schema.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource";
|
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
5
5
|
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema";
|
|
6
6
|
import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema";
|
|
7
|
+
import { messageActionsSchemaDefinitions } from "./message-actions";
|
|
7
8
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema";
|
|
8
9
|
import { contentActions } from "../functions/content-actions/resource";
|
|
9
10
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
@@ -285,8 +286,6 @@ const definitions = {
|
|
|
285
286
|
referenceId: a.id().required(),
|
|
286
287
|
status: a.string().required(),
|
|
287
288
|
reasonCode: a.string(),
|
|
288
|
-
messageId: a.id(),
|
|
289
|
-
relationId: a.id(),
|
|
290
289
|
}),
|
|
291
290
|
|
|
292
291
|
ReferenceQualityActionResult: a.customType({
|
|
@@ -582,6 +581,13 @@ const definitions = {
|
|
|
582
581
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
583
582
|
.handler(functionHandler("contentActions", contentActions)),
|
|
584
583
|
|
|
584
|
+
correctReferenceCapture: a
|
|
585
|
+
.mutation()
|
|
586
|
+
.arguments({ input: a.json().required() })
|
|
587
|
+
.returns(a.json())
|
|
588
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
589
|
+
.handler(functionHandler("contentActions", contentActions)),
|
|
590
|
+
|
|
585
591
|
saveItemDraft: a
|
|
586
592
|
.mutation()
|
|
587
593
|
.arguments({ input: a.json().required() })
|
|
@@ -1579,8 +1585,26 @@ const definitions = {
|
|
|
1579
1585
|
curationStatusUpdatedAt: a.datetime(),
|
|
1580
1586
|
curationStatusUpdatedBy: a.string(),
|
|
1581
1587
|
curationStatusReason: a.string(),
|
|
1588
|
+
curationReasonCode: a.string(),
|
|
1589
|
+
curationActorKind: a.string(),
|
|
1590
|
+
curationSectionId: a.string(),
|
|
1591
|
+
curationShareable: a.boolean(),
|
|
1592
|
+
curationDelegation: a.json(),
|
|
1593
|
+
curationDecisionRelationId: a.string(),
|
|
1582
1594
|
newsroomFeedKey: a.string(),
|
|
1583
1595
|
reviewedFeedKey: a.string(),
|
|
1596
|
+
discoveredByMonitorId: a.string(),
|
|
1597
|
+
discoveryDetails: a.json(),
|
|
1598
|
+
discoveryRule: a.json(),
|
|
1599
|
+
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1600
|
+
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1601
|
+
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
1602
|
+
// P(accept), written once, and never rewritten by a vote.
|
|
1603
|
+
feedStateKey: a.string(),
|
|
1604
|
+
feedArrivalAt: a.datetime(),
|
|
1605
|
+
predictedAcceptProbability: a.float(),
|
|
1606
|
+
predictionDecidedAt: a.datetime(),
|
|
1607
|
+
predictionSortKey: a.string(),
|
|
1584
1608
|
updatedAt: a.datetime(),
|
|
1585
1609
|
})
|
|
1586
1610
|
.secondaryIndexes((index) => [
|
|
@@ -1593,6 +1617,9 @@ const definitions = {
|
|
|
1593
1617
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1594
1618
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1595
1619
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1620
|
+
index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
|
|
1621
|
+
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1622
|
+
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1596
1623
|
])
|
|
1597
1624
|
.authorization((allow) => [
|
|
1598
1625
|
allow.groups(categoryWriteGroups).to(["read"]),
|
|
@@ -1680,6 +1707,9 @@ const definitions = {
|
|
|
1680
1707
|
authorSub: a.string(),
|
|
1681
1708
|
authorUserProfileId: a.id(),
|
|
1682
1709
|
authorLabel: a.string(),
|
|
1710
|
+
editedAt: a.datetime(),
|
|
1711
|
+
deletedAt: a.datetime(),
|
|
1712
|
+
deletedBySub: a.string(),
|
|
1683
1713
|
threadId: a.id(),
|
|
1684
1714
|
parentMessageId: a.id(),
|
|
1685
1715
|
sequenceNumber: a.integer(),
|
|
@@ -1714,20 +1744,25 @@ const definitions = {
|
|
|
1714
1744
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1715
1745
|
])
|
|
1716
1746
|
.authorization((allow) => [
|
|
1717
|
-
|
|
1747
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1748
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1749
|
+
allow.group("editor").to(["read"]),
|
|
1750
|
+
allow.group(adminGroup).to(["read"]),
|
|
1718
1751
|
]),
|
|
1719
1752
|
|
|
1720
1753
|
MessageThread: realtimeModel({
|
|
1721
1754
|
id: a.id().required(),
|
|
1722
1755
|
threadKind: a.string().required(),
|
|
1723
1756
|
status: a.string().required(),
|
|
1724
|
-
title
|
|
1757
|
+
// The title and the creator are set when a conversation is created and read afterwards; a title changes only through
|
|
1758
|
+
// renameThread (its creator or an admin), and the creator never changes.
|
|
1759
|
+
title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1725
1760
|
summary: a.string(),
|
|
1726
1761
|
primaryAnchorKind: a.string(),
|
|
1727
1762
|
primaryAnchorId: a.id(),
|
|
1728
1763
|
primaryAnchorLineageId: a.id(),
|
|
1729
1764
|
primaryAnchorKey: a.string(),
|
|
1730
|
-
createdBySub: a.string(),
|
|
1765
|
+
createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1731
1766
|
createdByUserProfileId: a.id(),
|
|
1732
1767
|
createdByLabel: a.string(),
|
|
1733
1768
|
messageCount: a.integer(),
|
|
@@ -2227,6 +2262,7 @@ const definitions = {
|
|
|
2227
2262
|
...monitoringViewSchemaDefinitions,
|
|
2228
2263
|
...automationViewSchemaDefinitions,
|
|
2229
2264
|
...proposalActionsSchemaDefinitions,
|
|
2265
|
+
...messageActionsSchemaDefinitions,
|
|
2230
2266
|
};
|
|
2231
2267
|
if (!includeEditions) {
|
|
2232
2268
|
const mutableDefinitions = definitions as Record<string, unknown>;
|
|
@@ -4,7 +4,7 @@ import { Amplify } from "aws-amplify";
|
|
|
4
4
|
import { generateClient } from "aws-amplify/data";
|
|
5
5
|
import type { Schema } from "../../data/resource";
|
|
6
6
|
import { putJsonModelPayload, putTextModelPayload, readJsonModelPayload } from "../shared/model-payloads";
|
|
7
|
-
import { curationActorKind,
|
|
7
|
+
import { curationActorKind, referenceVoteFields, validatedCurationSectionId } from "../shared/reference-curation-metadata";
|
|
8
8
|
import {
|
|
9
9
|
CURATION_DELEGATION_OWNER_TYPE,
|
|
10
10
|
CURATION_DELEGATION_PAYLOAD_KIND,
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
requestDelegation,
|
|
25
25
|
type DelegationStore,
|
|
26
26
|
} from "../shared/curation-delegation-flow";
|
|
27
|
+
import { rejectionReasonCodeForWrittenReason } from "../../../lib/reference-policy";
|
|
28
|
+
import { referenceFeedKeys, referenceFeedStateKey } from "../../../lib/reference-feed-keys";
|
|
29
|
+
import { claimGroups } from "../../../lib/staff-groups";
|
|
27
30
|
|
|
28
31
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
29
32
|
type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
|
|
@@ -213,7 +216,15 @@ async function reviewReferenceCuration(
|
|
|
213
216
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
214
217
|
const action = normalizeReferenceCurationAction(event.arguments.action);
|
|
215
218
|
const nextStatus = referenceCurationStatusForAction(action);
|
|
216
|
-
const
|
|
219
|
+
const writtenReason = normalizeOptionalString(event.arguments.note);
|
|
220
|
+
// A rejection needs a reason, and the editor's own sentence is one: with no reason code sent, the
|
|
221
|
+
// words are read for the code they name, so the scope-training negatives keep arriving from the
|
|
222
|
+
// surfaces that ask for words instead of a chip. Words that name no code are filed under "other".
|
|
223
|
+
const reasonCode = normalizeReferenceRejectionReasonCode(event.arguments.reasonCode)
|
|
224
|
+
?? (action === "reject" ? rejectionReasonCodeForWrittenReason(writtenReason) : null);
|
|
225
|
+
if (action === "reject" && !reasonCode) {
|
|
226
|
+
throw new Error("A rejection needs a reason: say why in your own words, or send a reasonCode.");
|
|
227
|
+
}
|
|
217
228
|
const reference = await getRequiredRecord(client.models.Reference, referenceId, "Reference");
|
|
218
229
|
const requestedSectionId = normalizeOptionalString(event.arguments.sectionId);
|
|
219
230
|
const sectionRecord = requestedSectionId
|
|
@@ -224,18 +235,11 @@ async function reviewReferenceCuration(
|
|
|
224
235
|
const actorSub = normalizeOptionalString(event.arguments.actorSub) ?? getIdentitySub(event);
|
|
225
236
|
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
226
237
|
const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
|
|
227
|
-
const note =
|
|
228
|
-
const
|
|
229
|
-
const referenceVersionNumber = typeof reference.versionNumber === "number" ? reference.versionNumber : null;
|
|
230
|
-
const referenceTitle = normalizeOptionalString(reference.title) ?? normalizeOptionalString(reference.externalItemId) ?? referenceId;
|
|
231
|
-
const messageId = `message-reference-curation-${safeId(referenceLineageId)}-${safeId(action)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
232
|
-
const relationId = `semantic-relation-${hashStable([
|
|
233
|
-
`message#${messageId}`,
|
|
234
|
-
"comment",
|
|
235
|
-
`reference#${referenceId}`,
|
|
236
|
-
action,
|
|
237
|
-
]).slice(0, 16)}`;
|
|
238
|
+
const note = writtenReason;
|
|
239
|
+
const decisionRelationId = normalizeOptionalString(event.arguments.decisionRelationId);
|
|
238
240
|
|
|
241
|
+
// The vote is recorded on the Reference itself: its status, who set it and the note, plus the details of the vote.
|
|
242
|
+
// Messages come only from people, so a vote writes no Message and no comment relation.
|
|
239
243
|
await requireDataResult(
|
|
240
244
|
client.models.Reference.update({
|
|
241
245
|
id: referenceId,
|
|
@@ -244,98 +248,23 @@ async function reviewReferenceCuration(
|
|
|
244
248
|
curationStatusUpdatedAt: now,
|
|
245
249
|
curationStatusUpdatedBy: actor,
|
|
246
250
|
curationStatusReason: note,
|
|
251
|
+
...referenceVoteFields({
|
|
252
|
+
actorKind: delegation ? "human" : curationActorKind(event.identity),
|
|
253
|
+
reasonCode,
|
|
254
|
+
sectionId,
|
|
255
|
+
shareable: event.arguments.shareable ?? null,
|
|
256
|
+
delegation,
|
|
257
|
+
decisionRelationId,
|
|
258
|
+
}),
|
|
247
259
|
newsroomFeedKey: reference.newsroomFeedKey ?? "references",
|
|
248
260
|
reviewedFeedKey: reviewedFeedKeyForStatus(nextStatus),
|
|
261
|
+
// A vote moves the Reference between the unrated and rated feeds; its original prediction and arrival stay as they were.
|
|
262
|
+
feedStateKey: referenceFeedStateKey({ ...reference, curationStatus: nextStatus }),
|
|
249
263
|
updatedAt: now,
|
|
250
264
|
}),
|
|
251
265
|
"update Reference curation status",
|
|
252
266
|
);
|
|
253
267
|
|
|
254
|
-
await requireDataResult(
|
|
255
|
-
client.models.Message.create(buildCanonicalMessageInput({
|
|
256
|
-
id: messageId,
|
|
257
|
-
messageKind: "reference_curation",
|
|
258
|
-
messageDomain: "commentary",
|
|
259
|
-
summary: `${referenceTitle}: ${nextStatus}`,
|
|
260
|
-
source: "newsroom",
|
|
261
|
-
authorSub: actorSub,
|
|
262
|
-
authorLabel: actor,
|
|
263
|
-
now,
|
|
264
|
-
})),
|
|
265
|
-
"create Message",
|
|
266
|
-
);
|
|
267
|
-
await putTextModelPayload(
|
|
268
|
-
client as any,
|
|
269
|
-
{ ownerKind: "message", ownerId: messageId, ownerLineageId: messageId },
|
|
270
|
-
"message_body",
|
|
271
|
-
"message",
|
|
272
|
-
note ?? `${actor} marked this reference ${nextStatus}.`,
|
|
273
|
-
{ filename: "message.txt", now },
|
|
274
|
-
);
|
|
275
|
-
await putJsonModelPayload(
|
|
276
|
-
client as any,
|
|
277
|
-
{ ownerKind: "message", ownerId: messageId, ownerLineageId: messageId },
|
|
278
|
-
"metadata",
|
|
279
|
-
"metadata",
|
|
280
|
-
referenceCurationMetadata({
|
|
281
|
-
action,
|
|
282
|
-
curationStatus: nextStatus,
|
|
283
|
-
reasonCode,
|
|
284
|
-
referenceId,
|
|
285
|
-
referenceLineageId,
|
|
286
|
-
decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
|
|
287
|
-
shareable: event.arguments.shareable ?? null,
|
|
288
|
-
actorKind: delegation ? "human" : curationActorKind(event.identity),
|
|
289
|
-
sectionId,
|
|
290
|
-
delegation,
|
|
291
|
-
}),
|
|
292
|
-
{ filename: "metadata.json", now },
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
await requireDataResult(
|
|
296
|
-
client.models.SemanticRelation.create({
|
|
297
|
-
id: relationId,
|
|
298
|
-
relationState: "current",
|
|
299
|
-
predicate: "comment",
|
|
300
|
-
relationTypeId: "semantic-relation-type-comment",
|
|
301
|
-
relationTypeKey: "comment",
|
|
302
|
-
relationDomain: "commentary",
|
|
303
|
-
subjectKind: "message",
|
|
304
|
-
subjectId: messageId,
|
|
305
|
-
subjectLineageId: messageId,
|
|
306
|
-
subjectVersionNumber: 1,
|
|
307
|
-
objectKind: "reference",
|
|
308
|
-
objectId: referenceId,
|
|
309
|
-
objectLineageId: referenceLineageId,
|
|
310
|
-
objectVersionNumber: referenceVersionNumber,
|
|
311
|
-
subjectStateKey: semanticStateKey("message", messageId),
|
|
312
|
-
objectStateKey: semanticStateKey("reference", referenceLineageId),
|
|
313
|
-
objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
|
|
314
|
-
predicateObjectStateKey: `comment#${semanticStateKey("reference", referenceLineageId)}`,
|
|
315
|
-
subjectVersionKey: semanticVersionKey("message", messageId),
|
|
316
|
-
objectVersionKey: semanticVersionKey("reference", referenceId),
|
|
317
|
-
score: 1,
|
|
318
|
-
confidence: null,
|
|
319
|
-
rank: 1,
|
|
320
|
-
classifierId: null,
|
|
321
|
-
modelVersion: null,
|
|
322
|
-
reviewRecommended: false,
|
|
323
|
-
sourceSnapshotId: null,
|
|
324
|
-
importRunId: null,
|
|
325
|
-
importedAt: now,
|
|
326
|
-
createdAt: now,
|
|
327
|
-
updatedAt: now,
|
|
328
|
-
newsroomFeedKey: "semanticRelations",
|
|
329
|
-
metadata: JSON.stringify({
|
|
330
|
-
action,
|
|
331
|
-
curationStatus: nextStatus,
|
|
332
|
-
reasonCode,
|
|
333
|
-
messageKind: "reference_curation",
|
|
334
|
-
}),
|
|
335
|
-
}),
|
|
336
|
-
"create Message SemanticRelation",
|
|
337
|
-
);
|
|
338
|
-
|
|
339
268
|
await updateNewsroomSummaryForReferenceCuration(client, {
|
|
340
269
|
corpusId: normalizeOptionalString(reference.corpusId),
|
|
341
270
|
previousStatus: normalizeOptionalString(reference.curationStatus) ?? "pending",
|
|
@@ -349,8 +278,6 @@ async function reviewReferenceCuration(
|
|
|
349
278
|
referenceId,
|
|
350
279
|
status: nextStatus,
|
|
351
280
|
reasonCode,
|
|
352
|
-
messageId,
|
|
353
|
-
relationId,
|
|
354
281
|
};
|
|
355
282
|
}
|
|
356
283
|
|
|
@@ -360,7 +287,7 @@ function requireSignedInPerson(event: { identity?: unknown }): { sub: string; la
|
|
|
360
287
|
}
|
|
361
288
|
const identity = event.identity as { sub?: unknown; claims?: Record<string, unknown> };
|
|
362
289
|
const claims = identity.claims ?? {};
|
|
363
|
-
const groups =
|
|
290
|
+
const groups = claimGroups(claims["cognito:groups"]);
|
|
364
291
|
return {
|
|
365
292
|
sub: normalizeRequiredString(claims.sub ?? identity.sub, "identity.sub"),
|
|
366
293
|
label: normalizeOptionalString(claims.email) ?? normalizeOptionalString(claims.name) ?? "Newsroom editor",
|
|
@@ -632,6 +559,8 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
|
|
|
632
559
|
curationStatusReason: note,
|
|
633
560
|
newsroomFeedKey: reference.newsroomFeedKey ?? "references",
|
|
634
561
|
reviewedFeedKey: reviewedFeedKeyForStatus(nextStatus),
|
|
562
|
+
// A vote moves the Reference between the unrated and rated feeds; its original prediction and arrival stay as they were.
|
|
563
|
+
feedStateKey: referenceFeedStateKey({ ...reference, curationStatus: nextStatus }),
|
|
635
564
|
updatedAt: now,
|
|
636
565
|
}),
|
|
637
566
|
"update Reference quality status",
|
|
@@ -808,6 +737,7 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
|
|
|
808
737
|
client.models.Reference.update({
|
|
809
738
|
id: normalizeRequiredString(current.id, "reference.id"),
|
|
810
739
|
versionState: "superseded",
|
|
740
|
+
feedStateKey: null, // a superseded version is in no feed
|
|
811
741
|
updatedAt: now,
|
|
812
742
|
}),
|
|
813
743
|
"supersede current Reference version",
|
|
@@ -845,8 +775,27 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
|
|
|
845
775
|
curationStatusUpdatedAt: normalizeOptionalString(current.curationStatusUpdatedAt) ?? now,
|
|
846
776
|
curationStatusUpdatedBy: normalizeOptionalString(current.curationStatusUpdatedBy) ?? actor,
|
|
847
777
|
curationStatusReason: normalizeOptionalString(current.curationStatusReason),
|
|
778
|
+
curationReasonCode: normalizeOptionalString(current.curationReasonCode),
|
|
779
|
+
curationActorKind: normalizeOptionalString(current.curationActorKind),
|
|
780
|
+
curationSectionId: normalizeOptionalString(current.curationSectionId),
|
|
781
|
+
curationShareable: typeof current.curationShareable === "boolean" ? current.curationShareable : null,
|
|
782
|
+
curationDelegation: current.curationDelegation ?? null,
|
|
783
|
+
curationDecisionRelationId: normalizeOptionalString(current.curationDecisionRelationId),
|
|
848
784
|
newsroomFeedKey: normalizeOptionalString(current.newsroomFeedKey) ?? "references",
|
|
849
785
|
reviewedFeedKey: normalizeOptionalString(current.reviewedFeedKey) ?? reviewedFeedKeyForStatus(curationStatus),
|
|
786
|
+
predictedAcceptProbability: typeof current.predictedAcceptProbability === "number" ? current.predictedAcceptProbability : null,
|
|
787
|
+
predictionDecidedAt: normalizeOptionalString(current.predictionDecidedAt),
|
|
788
|
+
...referenceFeedKeys({
|
|
789
|
+
id: nextReferenceId,
|
|
790
|
+
corpusId: targetCorpusId,
|
|
791
|
+
curationStatus,
|
|
792
|
+
versionState: "current",
|
|
793
|
+
predictedAcceptProbability: typeof current.predictedAcceptProbability === "number" ? current.predictedAcceptProbability : null,
|
|
794
|
+
createdAt: normalizeOptionalString(current.createdAt) ?? now,
|
|
795
|
+
importedAt: normalizeOptionalString(current.importedAt),
|
|
796
|
+
retrievedAt: normalizeOptionalString(current.retrievedAt),
|
|
797
|
+
sourcePublishedAt: normalizeOptionalString(current.sourcePublishedAt),
|
|
798
|
+
}),
|
|
850
799
|
updatedAt: now,
|
|
851
800
|
}),
|
|
852
801
|
"create moved Reference version",
|
|
@@ -2094,12 +2043,10 @@ const REFERENCE_REJECTION_REASON_CODES = new Set([
|
|
|
2094
2043
|
"other",
|
|
2095
2044
|
]);
|
|
2096
2045
|
|
|
2097
|
-
|
|
2046
|
+
/** A supplied reason code, validated. Whether a rejection has a reason at all is the caller's rule. */
|
|
2047
|
+
function normalizeReferenceRejectionReasonCode(value: unknown): string | null {
|
|
2098
2048
|
const normalized = normalizePolicyToken(value);
|
|
2099
|
-
if (!normalized)
|
|
2100
|
-
if (required) throw new Error("reasonCode is required when rejecting a reference.");
|
|
2101
|
-
return null;
|
|
2102
|
-
}
|
|
2049
|
+
if (!normalized) return null;
|
|
2103
2050
|
if (!REFERENCE_REJECTION_REASON_CODES.has(normalized)) {
|
|
2104
2051
|
throw new Error(`Unsupported reference rejection reason code ${normalized}.`);
|
|
2105
2052
|
}
|
|
@@ -2161,9 +2108,6 @@ async function updateNewsroomSummaryForReferenceCuration(
|
|
|
2161
2108
|
payload.generatedAt = input.now;
|
|
2162
2109
|
payload.staleAt = new Date(Date.parse(input.now) + SUMMARY_STALE_AFTER_MS).toISOString();
|
|
2163
2110
|
payload.source = "incremental";
|
|
2164
|
-
payload.counts.messages = Math.max(0, (payload.counts.messages ?? 0) + 1);
|
|
2165
|
-
payload.counts.modelAttachments = Math.max(0, (payload.counts.modelAttachments ?? 0) + 2);
|
|
2166
|
-
payload.counts.semanticRelations = Math.max(0, (payload.counts.semanticRelations ?? 0) + 1);
|
|
2167
2111
|
if (input.previousStatus !== input.nextStatus) {
|
|
2168
2112
|
increment(payload.referenceStatusCounts, input.previousStatus, -1);
|
|
2169
2113
|
increment(payload.referenceStatusCounts, input.nextStatus, 1);
|
|
@@ -2175,23 +2119,6 @@ async function updateNewsroomSummaryForReferenceCuration(
|
|
|
2175
2119
|
increment(payload.facets.references.statusByCorpus[input.corpusId], input.nextStatus, 1);
|
|
2176
2120
|
}
|
|
2177
2121
|
}
|
|
2178
|
-
increment(payload.messageKindCounts, "reference_curation", 1);
|
|
2179
|
-
increment(payload.messageDomainCounts, "commentary", 1);
|
|
2180
|
-
increment(payload.facets.messages.byKind, "reference_curation", 1);
|
|
2181
|
-
increment(payload.facets.messages.byDomain, "commentary", 1);
|
|
2182
|
-
increment(payload.facets.messages.byStatus, "active", 1);
|
|
2183
|
-
if (!payload.facets.messages.domainByKind.reference_curation) payload.facets.messages.domainByKind.reference_curation = {};
|
|
2184
|
-
increment(payload.facets.messages.domainByKind.reference_curation, "commentary", 1);
|
|
2185
|
-
increment(payload.facets.modelAttachments.byOwnerKind, "message", 2);
|
|
2186
|
-
increment(payload.facets.modelAttachments.byRole, "message_body", 1);
|
|
2187
|
-
increment(payload.facets.modelAttachments.byRole, "metadata", 1);
|
|
2188
|
-
increment(payload.facets.modelAttachments.byMediaType, "text/plain", 1);
|
|
2189
|
-
increment(payload.facets.modelAttachments.byMediaType, "application/json", 1);
|
|
2190
|
-
increment(payload.facets.modelAttachments.byStatus, "active", 2);
|
|
2191
|
-
increment(payload.facets.semanticRelations.byRelationTypeKey, "comment", 1);
|
|
2192
|
-
increment(payload.facets.semanticRelations.byRelationDomain, "workflow", 1);
|
|
2193
|
-
increment(payload.facets.semanticRelations.bySubjectKind, "message", 1);
|
|
2194
|
-
increment(payload.facets.semanticRelations.byObjectKind, "reference", 1);
|
|
2195
2122
|
await upsertNewsroomSummaryPayload(client, payload, response.data, input.now);
|
|
2196
2123
|
}
|
|
2197
2124
|
|
|
@@ -365,6 +365,14 @@ def handler(event, context):
|
|
|
365
365
|
client, arguments, event.get("identity") or {}, receipt_store=receipt_store)
|
|
366
366
|
except IntakeError as error:
|
|
367
367
|
return {"ok": False, "errors": [{"code": "intake-rejected", "message": str(error)}]}
|
|
368
|
+
if field == "correctReferenceCapture":
|
|
369
|
+
from papyrus_content.reference_curation_store import CurationError, correct_reference_capture
|
|
370
|
+
|
|
371
|
+
client, _claims = create_authoring_client()
|
|
372
|
+
try:
|
|
373
|
+
return correct_reference_capture(client, arguments, event.get("identity") or {})
|
|
374
|
+
except CurationError as error:
|
|
375
|
+
return {"ok": False, "errors": [{"code": "correction-rejected", "message": str(error)}]}
|
|
368
376
|
if field == "deriveMarkus":
|
|
369
377
|
return derive_markus_action(arguments)
|
|
370
378
|
if field == "saveItemDraft":
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
10
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34"];
|
|
10
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34", "pypdf>=6.12.1,<7", "youtube-transcript-api>=1.2.4,<2"];
|
|
11
11
|
function installCheckoutRequirements(outputDir) {
|
|
12
12
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
13
13
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -10,7 +10,7 @@ import { pythonLambdaCode, type SiteBundleFile } from "../shared/python-bundle";
|
|
|
10
10
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
12
12
|
|
|
13
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34"];
|
|
13
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1", "humanus>=0.33,<0.34", "pypdf>=6.12.1,<7", "youtube-transcript-api>=1.2.4,<2"];
|
|
14
14
|
|
|
15
15
|
function installCheckoutRequirements(outputDir: string): void {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
@@ -14,6 +14,14 @@ export const monitoringViewSchemaDefinitions = {
|
|
|
14
14
|
.returns(a.json())
|
|
15
15
|
.authorization((allow) => [allow.groups(editors)])
|
|
16
16
|
.handler(functionHandler("monitoringView", monitoringView)),
|
|
17
|
+
// One more page of one Monitoring list (runs, routerRuns, items, emailSubmissions), after a cursor from
|
|
18
|
+
// the overview or a previous page. filter "held" lists only held email submissions, restarting from the newest.
|
|
19
|
+
monitoringPage: a
|
|
20
|
+
.query()
|
|
21
|
+
.arguments({ list: a.string().required(), cursor: a.string(), filter: a.string(), scope: a.string() })
|
|
22
|
+
.returns(a.json())
|
|
23
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
24
|
+
.handler(functionHandler("monitoringView", monitoringView)),
|
|
17
25
|
monitoringEmailSubmission: a
|
|
18
26
|
.query()
|
|
19
27
|
.arguments({ receiptId: a.string().required() })
|
|
@@ -26,6 +34,14 @@ export const monitoringViewSchemaDefinitions = {
|
|
|
26
34
|
.returns(a.json())
|
|
27
35
|
.authorization((allow) => [allow.groups(editors)])
|
|
28
36
|
.handler(functionHandler("monitoringView", monitoringView)),
|
|
37
|
+
// Accept ("release") or Reject ("discard") one held email submission. The function validates the
|
|
38
|
+
// signed-in editor and that the message is routed to this publication; the receiver applies it.
|
|
39
|
+
monitoringDecideEmailSubmission: a
|
|
40
|
+
.mutation()
|
|
41
|
+
.arguments({ receiptId: a.string().required(), decision: a.string().required(), reason: a.string() })
|
|
42
|
+
.returns(a.json())
|
|
43
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
44
|
+
.handler(functionHandler("monitoringView", monitoringView)),
|
|
29
45
|
monitoringResolveEmailArticleLink: a
|
|
30
46
|
.mutation()
|
|
31
47
|
.arguments({ receiptId: a.string().required(), candidateIndex: a.integer().required() })
|
|
@@ -16,6 +16,14 @@ export const monitoringViewSchemaDefinitions = {
|
|
|
16
16
|
.returns(a.json())
|
|
17
17
|
.authorization((allow) => [allow.groups(editors)])
|
|
18
18
|
.handler(functionHandler("monitoringView", monitoringView)),
|
|
19
|
+
// One more page of one Monitoring list (runs, routerRuns, items, emailSubmissions), after a cursor from
|
|
20
|
+
// the overview or a previous page. filter "held" lists only held email submissions, restarting from the newest.
|
|
21
|
+
monitoringPage: a
|
|
22
|
+
.query()
|
|
23
|
+
.arguments({ list: a.string().required(), cursor: a.string(), filter: a.string(), scope: a.string() })
|
|
24
|
+
.returns(a.json())
|
|
25
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
26
|
+
.handler(functionHandler("monitoringView", monitoringView)),
|
|
19
27
|
monitoringEmailSubmission: a
|
|
20
28
|
.query()
|
|
21
29
|
.arguments({ receiptId: a.string().required() })
|
|
@@ -28,6 +36,14 @@ export const monitoringViewSchemaDefinitions = {
|
|
|
28
36
|
.returns(a.json())
|
|
29
37
|
.authorization((allow) => [allow.groups(editors)])
|
|
30
38
|
.handler(functionHandler("monitoringView", monitoringView)),
|
|
39
|
+
// Accept ("release") or Reject ("discard") one held email submission. The function validates the
|
|
40
|
+
// signed-in editor and that the message is routed to this publication; the receiver applies it.
|
|
41
|
+
monitoringDecideEmailSubmission: a
|
|
42
|
+
.mutation()
|
|
43
|
+
.arguments({ receiptId: a.string().required(), decision: a.string().required(), reason: a.string() })
|
|
44
|
+
.returns(a.json())
|
|
45
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
46
|
+
.handler(functionHandler("monitoringView", monitoringView)),
|
|
31
47
|
monitoringResolveEmailArticleLink: a
|
|
32
48
|
.mutation()
|
|
33
49
|
.arguments({ receiptId: a.string().required(), candidateIndex: a.integer().required() })
|
|
@@ -3,6 +3,7 @@ import { getAmplifyDataClientConfig } from "@aws-amplify/backend/function/runtim
|
|
|
3
3
|
import { Amplify } from "aws-amplify";
|
|
4
4
|
import { generateClient } from "aws-amplify/data";
|
|
5
5
|
import type { Schema } from "../../data/resource";
|
|
6
|
+
import { claimGroups } from "../../../lib/staff-groups";
|
|
6
7
|
|
|
7
8
|
type DataClient = ReturnType<typeof generateClient<Schema>>;
|
|
8
9
|
type DataClientErrors = Array<{ message?: string | null } | string | null> | null | undefined;
|
|
@@ -474,20 +475,11 @@ function validateJsonSchemaInput(schema: Record<string, unknown>, input: Record<
|
|
|
474
475
|
|
|
475
476
|
function requireAdmin(event: ProcedureEvent): void {
|
|
476
477
|
const claims = objectValue(event.identity?.claims);
|
|
477
|
-
const groups = [
|
|
478
|
-
...normalizeClaimGroups(claims["cognito:groups"]),
|
|
479
|
-
...normalizeClaimGroups(claims.groups),
|
|
480
|
-
];
|
|
478
|
+
const groups = [...claimGroups(claims["cognito:groups"]), ...claimGroups(claims.groups)];
|
|
481
479
|
if (groups.includes("admin")) return;
|
|
482
480
|
throw new Error("Admin role required.");
|
|
483
481
|
}
|
|
484
482
|
|
|
485
|
-
function normalizeClaimGroups(value: unknown): string[] {
|
|
486
|
-
if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
487
|
-
if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
488
|
-
return [];
|
|
489
|
-
}
|
|
490
|
-
|
|
491
483
|
async function getDataClient(): Promise<DataClient> {
|
|
492
484
|
if (!clientPromise) {
|
|
493
485
|
clientPromise = (async () => {
|
|
@@ -92,6 +92,25 @@ def is_appsync_event(event: dict[str, Any]) -> bool:
|
|
|
92
92
|
return "arguments" in event and ("info" in event or event_field_name(event) is not None)
|
|
93
93
|
PROPOSER_AI = "ai"
|
|
94
94
|
STAFF_GROUPS = frozenset({"editor", "admin"})
|
|
95
|
+
|
|
96
|
+
_GROUP_SEPARATORS = re.compile(r"[\s,]+")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def claim_groups(value: Any) -> list[str]:
|
|
100
|
+
"""Copy of ``papyrus_content.staff_groups.claim_groups`` (this function ships without that package; a test keeps them equal)."""
|
|
101
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
102
|
+
raw = [item for item in value if isinstance(item, str)]
|
|
103
|
+
elif isinstance(value, str):
|
|
104
|
+
raw = _GROUP_SEPARATORS.split(value)
|
|
105
|
+
else:
|
|
106
|
+
return []
|
|
107
|
+
cleaned = (item.strip().strip("\"'[](){}").strip() for item in raw)
|
|
108
|
+
return [item for item in cleaned if item]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def has_editor_rights(groups: Any) -> bool:
|
|
112
|
+
"""``admin`` has every editor right: an admin who is not in the ``editor`` group is staff."""
|
|
113
|
+
return any(group.lower() in STAFF_GROUPS for group in claim_groups(groups))
|
|
95
114
|
#: The only moves an operator's IAM role may make, as (from, to). Everything after the draft is an editor's decision.
|
|
96
115
|
OPERATOR_TRANSITIONS = frozenset({(DRAFT, IN_STEERING), (DRAFT, DECLINED)})
|
|
97
116
|
BY_STAFF, BY_OPERATOR = "staff", "operator"
|
|
@@ -140,10 +159,8 @@ def staff_subject(identity: dict[str, Any]) -> str | None:
|
|
|
140
159
|
"""The Cognito subject of an editor or admin, or ``None`` for anyone else."""
|
|
141
160
|
claims = identity.get("claims") or {}
|
|
142
161
|
groups = claims.get("cognito:groups") or identity.get("groups") or []
|
|
143
|
-
if isinstance(groups, str):
|
|
144
|
-
groups = [group.strip() for group in groups.strip("[]").replace(",", " ").split()]
|
|
145
162
|
subject = identity.get("sub")
|
|
146
|
-
if not isinstance(subject, str) or not subject or not
|
|
163
|
+
if not isinstance(subject, str) or not subject or not has_editor_rights(groups):
|
|
147
164
|
return None
|
|
148
165
|
return subject
|
|
149
166
|
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* relevance cyclotron decided the reference, the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The details of a vote, kept on the Reference itself (Messages come only from people).
|
|
3
|
+
* When the relevance cyclotron decided the reference, the vote records which decision
|
|
4
|
+
* it answered and whether its explanation may be shared (for the Cyclotron marketing
|
|
5
|
+
* recording). Both are optional. A Reference keeps only its latest vote.
|
|
6
6
|
*/
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
referenceId: string
|
|
12
|
-
referenceLineageId: string
|
|
7
|
+
export type ReferenceVoteInput = {
|
|
8
|
+
actorKind: CurationActorKind
|
|
9
|
+
reasonCode?: string | null
|
|
10
|
+
sectionId?: string | null
|
|
13
11
|
decisionRelationId?: string | null
|
|
14
12
|
shareable?: boolean | null
|
|
15
|
-
actorKind?: CurationActorKind | null
|
|
16
|
-
sectionId?: string | null
|
|
17
13
|
delegation?: { delegationId: string; relayedBy: string; instruction: string } | null
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
export type CurationActorKind = "human" | "
|
|
16
|
+
export type CurationActorKind = "human" | "agent" | "cyclotron" | "rule" | "catalog"
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
19
|
* A curation is human only when the caller signed in through the user pool, so
|
|
@@ -27,26 +23,22 @@ export type CurationActorKind = "human" | "service"
|
|
|
27
23
|
export function curationActorKind(identity: unknown): CurationActorKind {
|
|
28
24
|
const claims = (identity as {claims?: Record<string, unknown>} | null | undefined)?.claims
|
|
29
25
|
const subject = claims?.sub
|
|
30
|
-
return typeof subject === "string" && subject.trim() !== "" ? "human" : "
|
|
26
|
+
return typeof subject === "string" && subject.trim() !== "" ? "human" : "agent"
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The Reference fields a vote sets. Every field is always present, so a new vote clears
|
|
31
|
+
* what the last one carried. The delegation is stored as JSON.
|
|
32
|
+
*/
|
|
33
|
+
export function referenceVoteFields(input: ReferenceVoteInput): Record<string, unknown> {
|
|
34
34
|
const decisionRelationId = input.decisionRelationId?.trim() || null
|
|
35
35
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
...(input.actorKind ? {actorKind: input.actorKind} : {}),
|
|
43
|
-
...(input.sectionId?.trim() ? {sectionId: input.sectionId.trim()} : {}),
|
|
44
|
-
...(input.delegation
|
|
45
|
-
? {attestation: "delegated", relayedBy: input.delegation.relayedBy, delegationId: input.delegation.delegationId,
|
|
46
|
-
instruction: input.delegation.instruction}
|
|
47
|
-
: {}),
|
|
48
|
-
...(decisionRelationId ? {decisionRelationId} : {}),
|
|
49
|
-
...(decisionRelationId ? {shareable: input.shareable === true} : {}),
|
|
36
|
+
curationActorKind: input.actorKind,
|
|
37
|
+
curationReasonCode: input.reasonCode?.trim() || null,
|
|
38
|
+
curationSectionId: input.sectionId?.trim() || null,
|
|
39
|
+
curationShareable: decisionRelationId ? input.shareable === true : null,
|
|
40
|
+
curationDelegation: input.delegation ? JSON.stringify(input.delegation) : null,
|
|
41
|
+
curationDecisionRelationId: decisionRelationId,
|
|
50
42
|
}
|
|
51
43
|
}
|
|
52
44
|
|