@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.
- 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 +28 -3
- package/amplify/data/resource.ts +28 -3
- package/amplify/data/schema.js +34 -5
- package/amplify/data/schema.ts +34 -5
- package/amplify/functions/category-action/handler.ts +51 -125
- package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
- package/components/conversation-context.tsx +29 -2
- package/components/conversation-provider.tsx +5 -1
- package/components/conversation-thread.tsx +154 -25
- package/components/conversations-memory-source.ts +83 -0
- package/components/conversations-network-source.ts +52 -0
- package/components/conversations-page.tsx +14 -0
- package/components/conversations-source.ts +36 -0
- package/components/conversations-view.tsx +680 -0
- package/components/decision-control.tsx +307 -0
- package/components/list-detail-layout.tsx +44 -0
- package/components/needs-me-bodies.tsx +18 -17
- package/components/needs-me-feed.tsx +16 -32
- package/components/news-desk-taxonomy-client.ts +216 -62
- package/components/newsroom-article-editor.tsx +10 -4
- package/components/newsroom-organizer.css +34 -1
- package/components/newsroom-references-view.tsx +41 -100
- package/components/notification-cards.tsx +9 -8
- package/components/notification-center.tsx +5 -7
- package/components/proposals-view.tsx +4 -4
- package/components/reference-conversation.tsx +5 -0
- package/components/reference-feed.tsx +129 -127
- package/components/reference-review-card.tsx +9 -135
- package/components/references-page-loading.tsx +12 -6
- package/components/story-reporting-trail-client.ts +4 -3
- package/components/story-reporting-trail.tsx +8 -0
- package/components/topic-steering-workspace.tsx +44 -786
- package/components/use-conversation.ts +36 -2
- package/components/use-feed-preferences.ts +33 -0
- package/components/use-reference-ratings.ts +11 -7
- package/lib/category-repository.ts +14 -38
- package/lib/console-chat-client.ts +8 -178
- package/lib/conversation-feed.ts +96 -0
- package/lib/conversation.ts +73 -2
- package/lib/cyclotron-suggestions.ts +18 -36
- package/lib/local-preview-seed.ts +16 -27
- package/lib/needs-me-items.ts +4 -4
- package/lib/needs-me-ranking.ts +9 -22
- package/lib/newsroom-board.ts +10 -1
- package/lib/newsroom-demo-dashboard.ts +0 -7
- package/lib/newsroom-organizer-nav.ts +2 -2
- package/lib/post-message.ts +54 -0
- package/lib/proposals-admin.ts +2 -2
- package/lib/reference-feed-keys.ts +69 -0
- package/lib/reference-feed-ranking.ts +77 -160
- package/lib/reference-policy.ts +83 -11
- package/lib/reference-review-details.ts +21 -57
- package/lib/relevance-decisions.ts +6 -2
- package/lib/review-drafts.ts +2 -2
- package/lib/review-fixture.ts +1 -1
- package/lib/review-requests.ts +4 -4
- package/lib/semantic-graph.ts +1 -1
- package/lib/story-reporting-trail.ts +28 -2
- package/package.json +12 -2
- package/routes.manifest.json +1 -1
- package/components/review-rating-grid.tsx +0 -190
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({
|
|
@@ -1586,8 +1585,26 @@ const definitions = {
|
|
|
1586
1585
|
curationStatusUpdatedAt: a.datetime(),
|
|
1587
1586
|
curationStatusUpdatedBy: a.string(),
|
|
1588
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(),
|
|
1589
1594
|
newsroomFeedKey: a.string(),
|
|
1590
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(),
|
|
1591
1608
|
updatedAt: a.datetime(),
|
|
1592
1609
|
})
|
|
1593
1610
|
.secondaryIndexes((index) => [
|
|
@@ -1600,6 +1617,9 @@ const definitions = {
|
|
|
1600
1617
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1601
1618
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1602
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"),
|
|
1603
1623
|
])
|
|
1604
1624
|
.authorization((allow) => [
|
|
1605
1625
|
allow.groups(categoryWriteGroups).to(["read"]),
|
|
@@ -1687,6 +1707,9 @@ const definitions = {
|
|
|
1687
1707
|
authorSub: a.string(),
|
|
1688
1708
|
authorUserProfileId: a.id(),
|
|
1689
1709
|
authorLabel: a.string(),
|
|
1710
|
+
editedAt: a.datetime(),
|
|
1711
|
+
deletedAt: a.datetime(),
|
|
1712
|
+
deletedBySub: a.string(),
|
|
1690
1713
|
threadId: a.id(),
|
|
1691
1714
|
parentMessageId: a.id(),
|
|
1692
1715
|
sequenceNumber: a.integer(),
|
|
@@ -1721,20 +1744,25 @@ const definitions = {
|
|
|
1721
1744
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1722
1745
|
])
|
|
1723
1746
|
.authorization((allow) => [
|
|
1724
|
-
|
|
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"]),
|
|
1725
1751
|
]),
|
|
1726
1752
|
|
|
1727
1753
|
MessageThread: realtimeModel({
|
|
1728
1754
|
id: a.id().required(),
|
|
1729
1755
|
threadKind: a.string().required(),
|
|
1730
1756
|
status: a.string().required(),
|
|
1731
|
-
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"])]),
|
|
1732
1760
|
summary: a.string(),
|
|
1733
1761
|
primaryAnchorKind: a.string(),
|
|
1734
1762
|
primaryAnchorId: a.id(),
|
|
1735
1763
|
primaryAnchorLineageId: a.id(),
|
|
1736
1764
|
primaryAnchorKey: a.string(),
|
|
1737
|
-
createdBySub: a.string(),
|
|
1765
|
+
createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1738
1766
|
createdByUserProfileId: a.id(),
|
|
1739
1767
|
createdByLabel: a.string(),
|
|
1740
1768
|
messageCount: a.integer(),
|
|
@@ -2234,6 +2262,7 @@ const definitions = {
|
|
|
2234
2262
|
...monitoringViewSchemaDefinitions,
|
|
2235
2263
|
...automationViewSchemaDefinitions,
|
|
2236
2264
|
...proposalActionsSchemaDefinitions,
|
|
2265
|
+
...messageActionsSchemaDefinitions,
|
|
2237
2266
|
};
|
|
2238
2267
|
if (!includeEditions) {
|
|
2239
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,8 @@ 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";
|
|
27
29
|
import { claimGroups } from "../../../lib/staff-groups";
|
|
28
30
|
|
|
29
31
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
@@ -214,7 +216,15 @@ async function reviewReferenceCuration(
|
|
|
214
216
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
215
217
|
const action = normalizeReferenceCurationAction(event.arguments.action);
|
|
216
218
|
const nextStatus = referenceCurationStatusForAction(action);
|
|
217
|
-
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
|
+
}
|
|
218
228
|
const reference = await getRequiredRecord(client.models.Reference, referenceId, "Reference");
|
|
219
229
|
const requestedSectionId = normalizeOptionalString(event.arguments.sectionId);
|
|
220
230
|
const sectionRecord = requestedSectionId
|
|
@@ -225,18 +235,11 @@ async function reviewReferenceCuration(
|
|
|
225
235
|
const actorSub = normalizeOptionalString(event.arguments.actorSub) ?? getIdentitySub(event);
|
|
226
236
|
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
227
237
|
const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
|
|
228
|
-
const note =
|
|
229
|
-
const
|
|
230
|
-
const referenceVersionNumber = typeof reference.versionNumber === "number" ? reference.versionNumber : null;
|
|
231
|
-
const referenceTitle = normalizeOptionalString(reference.title) ?? normalizeOptionalString(reference.externalItemId) ?? referenceId;
|
|
232
|
-
const messageId = `message-reference-curation-${safeId(referenceLineageId)}-${safeId(action)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
233
|
-
const relationId = `semantic-relation-${hashStable([
|
|
234
|
-
`message#${messageId}`,
|
|
235
|
-
"comment",
|
|
236
|
-
`reference#${referenceId}`,
|
|
237
|
-
action,
|
|
238
|
-
]).slice(0, 16)}`;
|
|
238
|
+
const note = writtenReason;
|
|
239
|
+
const decisionRelationId = normalizeOptionalString(event.arguments.decisionRelationId);
|
|
239
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.
|
|
240
243
|
await requireDataResult(
|
|
241
244
|
client.models.Reference.update({
|
|
242
245
|
id: referenceId,
|
|
@@ -245,98 +248,23 @@ async function reviewReferenceCuration(
|
|
|
245
248
|
curationStatusUpdatedAt: now,
|
|
246
249
|
curationStatusUpdatedBy: actor,
|
|
247
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
|
+
}),
|
|
248
259
|
newsroomFeedKey: reference.newsroomFeedKey ?? "references",
|
|
249
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 }),
|
|
250
263
|
updatedAt: now,
|
|
251
264
|
}),
|
|
252
265
|
"update Reference curation status",
|
|
253
266
|
);
|
|
254
267
|
|
|
255
|
-
await requireDataResult(
|
|
256
|
-
client.models.Message.create(buildCanonicalMessageInput({
|
|
257
|
-
id: messageId,
|
|
258
|
-
messageKind: "reference_curation",
|
|
259
|
-
messageDomain: "commentary",
|
|
260
|
-
summary: `${referenceTitle}: ${nextStatus}`,
|
|
261
|
-
source: "newsroom",
|
|
262
|
-
authorSub: actorSub,
|
|
263
|
-
authorLabel: actor,
|
|
264
|
-
now,
|
|
265
|
-
})),
|
|
266
|
-
"create Message",
|
|
267
|
-
);
|
|
268
|
-
await putTextModelPayload(
|
|
269
|
-
client as any,
|
|
270
|
-
{ ownerKind: "message", ownerId: messageId, ownerLineageId: messageId },
|
|
271
|
-
"message_body",
|
|
272
|
-
"message",
|
|
273
|
-
note ?? `${actor} marked this reference ${nextStatus}.`,
|
|
274
|
-
{ filename: "message.txt", now },
|
|
275
|
-
);
|
|
276
|
-
await putJsonModelPayload(
|
|
277
|
-
client as any,
|
|
278
|
-
{ ownerKind: "message", ownerId: messageId, ownerLineageId: messageId },
|
|
279
|
-
"metadata",
|
|
280
|
-
"metadata",
|
|
281
|
-
referenceCurationMetadata({
|
|
282
|
-
action,
|
|
283
|
-
curationStatus: nextStatus,
|
|
284
|
-
reasonCode,
|
|
285
|
-
referenceId,
|
|
286
|
-
referenceLineageId,
|
|
287
|
-
decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
|
|
288
|
-
shareable: event.arguments.shareable ?? null,
|
|
289
|
-
actorKind: delegation ? "human" : curationActorKind(event.identity),
|
|
290
|
-
sectionId,
|
|
291
|
-
delegation,
|
|
292
|
-
}),
|
|
293
|
-
{ filename: "metadata.json", now },
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
await requireDataResult(
|
|
297
|
-
client.models.SemanticRelation.create({
|
|
298
|
-
id: relationId,
|
|
299
|
-
relationState: "current",
|
|
300
|
-
predicate: "comment",
|
|
301
|
-
relationTypeId: "semantic-relation-type-comment",
|
|
302
|
-
relationTypeKey: "comment",
|
|
303
|
-
relationDomain: "commentary",
|
|
304
|
-
subjectKind: "message",
|
|
305
|
-
subjectId: messageId,
|
|
306
|
-
subjectLineageId: messageId,
|
|
307
|
-
subjectVersionNumber: 1,
|
|
308
|
-
objectKind: "reference",
|
|
309
|
-
objectId: referenceId,
|
|
310
|
-
objectLineageId: referenceLineageId,
|
|
311
|
-
objectVersionNumber: referenceVersionNumber,
|
|
312
|
-
subjectStateKey: semanticStateKey("message", messageId),
|
|
313
|
-
objectStateKey: semanticStateKey("reference", referenceLineageId),
|
|
314
|
-
objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
|
|
315
|
-
predicateObjectStateKey: `comment#${semanticStateKey("reference", referenceLineageId)}`,
|
|
316
|
-
subjectVersionKey: semanticVersionKey("message", messageId),
|
|
317
|
-
objectVersionKey: semanticVersionKey("reference", referenceId),
|
|
318
|
-
score: 1,
|
|
319
|
-
confidence: null,
|
|
320
|
-
rank: 1,
|
|
321
|
-
classifierId: null,
|
|
322
|
-
modelVersion: null,
|
|
323
|
-
reviewRecommended: false,
|
|
324
|
-
sourceSnapshotId: null,
|
|
325
|
-
importRunId: null,
|
|
326
|
-
importedAt: now,
|
|
327
|
-
createdAt: now,
|
|
328
|
-
updatedAt: now,
|
|
329
|
-
newsroomFeedKey: "semanticRelations",
|
|
330
|
-
metadata: JSON.stringify({
|
|
331
|
-
action,
|
|
332
|
-
curationStatus: nextStatus,
|
|
333
|
-
reasonCode,
|
|
334
|
-
messageKind: "reference_curation",
|
|
335
|
-
}),
|
|
336
|
-
}),
|
|
337
|
-
"create Message SemanticRelation",
|
|
338
|
-
);
|
|
339
|
-
|
|
340
268
|
await updateNewsroomSummaryForReferenceCuration(client, {
|
|
341
269
|
corpusId: normalizeOptionalString(reference.corpusId),
|
|
342
270
|
previousStatus: normalizeOptionalString(reference.curationStatus) ?? "pending",
|
|
@@ -350,8 +278,6 @@ async function reviewReferenceCuration(
|
|
|
350
278
|
referenceId,
|
|
351
279
|
status: nextStatus,
|
|
352
280
|
reasonCode,
|
|
353
|
-
messageId,
|
|
354
|
-
relationId,
|
|
355
281
|
};
|
|
356
282
|
}
|
|
357
283
|
|
|
@@ -633,6 +559,8 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
|
|
|
633
559
|
curationStatusReason: note,
|
|
634
560
|
newsroomFeedKey: reference.newsroomFeedKey ?? "references",
|
|
635
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 }),
|
|
636
564
|
updatedAt: now,
|
|
637
565
|
}),
|
|
638
566
|
"update Reference quality status",
|
|
@@ -809,6 +737,7 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
|
|
|
809
737
|
client.models.Reference.update({
|
|
810
738
|
id: normalizeRequiredString(current.id, "reference.id"),
|
|
811
739
|
versionState: "superseded",
|
|
740
|
+
feedStateKey: null, // a superseded version is in no feed
|
|
812
741
|
updatedAt: now,
|
|
813
742
|
}),
|
|
814
743
|
"supersede current Reference version",
|
|
@@ -846,8 +775,27 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
|
|
|
846
775
|
curationStatusUpdatedAt: normalizeOptionalString(current.curationStatusUpdatedAt) ?? now,
|
|
847
776
|
curationStatusUpdatedBy: normalizeOptionalString(current.curationStatusUpdatedBy) ?? actor,
|
|
848
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),
|
|
849
784
|
newsroomFeedKey: normalizeOptionalString(current.newsroomFeedKey) ?? "references",
|
|
850
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
|
+
}),
|
|
851
799
|
updatedAt: now,
|
|
852
800
|
}),
|
|
853
801
|
"create moved Reference version",
|
|
@@ -2095,12 +2043,10 @@ const REFERENCE_REJECTION_REASON_CODES = new Set([
|
|
|
2095
2043
|
"other",
|
|
2096
2044
|
]);
|
|
2097
2045
|
|
|
2098
|
-
|
|
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 {
|
|
2099
2048
|
const normalized = normalizePolicyToken(value);
|
|
2100
|
-
if (!normalized)
|
|
2101
|
-
if (required) throw new Error("reasonCode is required when rejecting a reference.");
|
|
2102
|
-
return null;
|
|
2103
|
-
}
|
|
2049
|
+
if (!normalized) return null;
|
|
2104
2050
|
if (!REFERENCE_REJECTION_REASON_CODES.has(normalized)) {
|
|
2105
2051
|
throw new Error(`Unsupported reference rejection reason code ${normalized}.`);
|
|
2106
2052
|
}
|
|
@@ -2162,9 +2108,6 @@ async function updateNewsroomSummaryForReferenceCuration(
|
|
|
2162
2108
|
payload.generatedAt = input.now;
|
|
2163
2109
|
payload.staleAt = new Date(Date.parse(input.now) + SUMMARY_STALE_AFTER_MS).toISOString();
|
|
2164
2110
|
payload.source = "incremental";
|
|
2165
|
-
payload.counts.messages = Math.max(0, (payload.counts.messages ?? 0) + 1);
|
|
2166
|
-
payload.counts.modelAttachments = Math.max(0, (payload.counts.modelAttachments ?? 0) + 2);
|
|
2167
|
-
payload.counts.semanticRelations = Math.max(0, (payload.counts.semanticRelations ?? 0) + 1);
|
|
2168
2111
|
if (input.previousStatus !== input.nextStatus) {
|
|
2169
2112
|
increment(payload.referenceStatusCounts, input.previousStatus, -1);
|
|
2170
2113
|
increment(payload.referenceStatusCounts, input.nextStatus, 1);
|
|
@@ -2176,23 +2119,6 @@ async function updateNewsroomSummaryForReferenceCuration(
|
|
|
2176
2119
|
increment(payload.facets.references.statusByCorpus[input.corpusId], input.nextStatus, 1);
|
|
2177
2120
|
}
|
|
2178
2121
|
}
|
|
2179
|
-
increment(payload.messageKindCounts, "reference_curation", 1);
|
|
2180
|
-
increment(payload.messageDomainCounts, "commentary", 1);
|
|
2181
|
-
increment(payload.facets.messages.byKind, "reference_curation", 1);
|
|
2182
|
-
increment(payload.facets.messages.byDomain, "commentary", 1);
|
|
2183
|
-
increment(payload.facets.messages.byStatus, "active", 1);
|
|
2184
|
-
if (!payload.facets.messages.domainByKind.reference_curation) payload.facets.messages.domainByKind.reference_curation = {};
|
|
2185
|
-
increment(payload.facets.messages.domainByKind.reference_curation, "commentary", 1);
|
|
2186
|
-
increment(payload.facets.modelAttachments.byOwnerKind, "message", 2);
|
|
2187
|
-
increment(payload.facets.modelAttachments.byRole, "message_body", 1);
|
|
2188
|
-
increment(payload.facets.modelAttachments.byRole, "metadata", 1);
|
|
2189
|
-
increment(payload.facets.modelAttachments.byMediaType, "text/plain", 1);
|
|
2190
|
-
increment(payload.facets.modelAttachments.byMediaType, "application/json", 1);
|
|
2191
|
-
increment(payload.facets.modelAttachments.byStatus, "active", 2);
|
|
2192
|
-
increment(payload.facets.semanticRelations.byRelationTypeKey, "comment", 1);
|
|
2193
|
-
increment(payload.facets.semanticRelations.byRelationDomain, "workflow", 1);
|
|
2194
|
-
increment(payload.facets.semanticRelations.bySubjectKind, "message", 1);
|
|
2195
|
-
increment(payload.facets.semanticRelations.byObjectKind, "reference", 1);
|
|
2196
2122
|
await upsertNewsroomSummaryPayload(client, payload, response.data, input.now);
|
|
2197
2123
|
}
|
|
2198
2124
|
|
|
@@ -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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
-
import type
|
|
4
|
+
import { NO_VIEWER, canDeleteMessage, canEditMessage, type ConversationAnchor, type ConversationMessage, type ConversationViewer } from "../lib/conversation";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Where conversations are read and written. The newsroom provides the network one; a demo or a test provides the in-memory one. A
|
|
@@ -14,6 +14,11 @@ export type ConversationStore = {
|
|
|
14
14
|
append: (input: { anchor: ConversationAnchor; body: string; parentMessageId: string | null }) => Promise<ConversationMessage>;
|
|
15
15
|
/** Message counts by Reference lineage, for the feed cards. */
|
|
16
16
|
counts: () => Promise<Map<string, number>>;
|
|
17
|
+
/** Who is looking, and display names by profile id: what Edit, Delete and the author's name are decided from. */
|
|
18
|
+
viewer: () => Promise<ConversationViewer>;
|
|
19
|
+
authors: (profileIds: string[]) => Promise<Map<string, string>>;
|
|
20
|
+
edit: (messageId: string, body: string) => Promise<ConversationMessage>;
|
|
21
|
+
remove: (messageId: string, moderate: boolean) => Promise<ConversationMessage>;
|
|
17
22
|
};
|
|
18
23
|
|
|
19
24
|
const ConversationStoreContext = createContext<ConversationStore | null>(null);
|
|
@@ -27,11 +32,32 @@ export function useConversationStore(): ConversationStore | null {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/** A store that keeps everything in memory: the demo newsroom, and the tests. */
|
|
30
|
-
export function createMemoryConversationStore(authorLabel: string, seed: Record<string, ConversationMessage[]> = {}): ConversationStore {
|
|
35
|
+
export function createMemoryConversationStore(authorLabel: string, seed: Record<string, ConversationMessage[]> = {}, who: { viewer?: ConversationViewer; profiles?: Record<string, string> } = {}): ConversationStore {
|
|
36
|
+
const viewer = who.viewer ?? NO_VIEWER;
|
|
31
37
|
const byAnchor = new Map<string, ConversationMessage[]>(Object.entries(seed));
|
|
32
38
|
const keyOf = (anchor: ConversationAnchor) => `${anchor.kind}#${anchor.lineageId ?? ""}`;
|
|
39
|
+
const change = (messageId: string, apply: (message: ConversationMessage) => ConversationMessage) => {
|
|
40
|
+
for (const [key, list] of byAnchor) {
|
|
41
|
+
const found = list.find((message) => message.id === messageId);
|
|
42
|
+
if (!found) continue;
|
|
43
|
+
const changed = apply(found);
|
|
44
|
+
byAnchor.set(key, list.map((message) => (message.id === messageId ? changed : message)));
|
|
45
|
+
return changed;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("That message is not here.");
|
|
48
|
+
};
|
|
33
49
|
return {
|
|
34
50
|
authorLabel,
|
|
51
|
+
viewer: async () => viewer,
|
|
52
|
+
authors: async (ids) => new Map(ids.filter((id) => who.profiles?.[id]).map((id) => [id, who.profiles![id]])),
|
|
53
|
+
edit: async (messageId, body) => change(messageId, (message) => {
|
|
54
|
+
if (!canEditMessage(viewer, message)) throw new Error("Only the author can edit a message.");
|
|
55
|
+
return { ...message, content: body.trim(), editedAt: new Date().toISOString() };
|
|
56
|
+
}),
|
|
57
|
+
remove: async (messageId) => change(messageId, (message) => {
|
|
58
|
+
if (!canDeleteMessage(viewer, message)) throw new Error("Only the author or an admin can delete a message.");
|
|
59
|
+
return { ...message, content: "", deleted: true, deletedAt: new Date().toISOString() };
|
|
60
|
+
}),
|
|
35
61
|
load: async (anchor) => [...(byAnchor.get(keyOf(anchor)) ?? [])],
|
|
36
62
|
append: async ({ anchor, body, parentMessageId }) => {
|
|
37
63
|
const list = byAnchor.get(keyOf(anchor)) ?? [];
|
|
@@ -40,6 +66,7 @@ export function createMemoryConversationStore(authorLabel: string, seed: Record<
|
|
|
40
66
|
id: `message-memory-${sequenceNumber}-${list.length}`,
|
|
41
67
|
parentMessageId,
|
|
42
68
|
authorLabel,
|
|
69
|
+
authorSub: viewer.sub,
|
|
43
70
|
content: body.trim(),
|
|
44
71
|
createdAt: new Date().toISOString(),
|
|
45
72
|
sequenceNumber,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, type ReactNode } from "react";
|
|
4
4
|
import { ConversationStoreProvider, createMemoryConversationStore, type ConversationStore } from "./conversation-context";
|
|
5
|
-
import { appendConversationMessage, loadConversationMessages, loadReferenceConversationCounts } from "./news-desk-taxonomy-client";
|
|
5
|
+
import { appendConversationMessage, deleteConversationMessage, editConversationMessage, loadConversationMessages, loadConversationViewer, loadReferenceConversationCounts, loadUserProfileNames } from "./news-desk-taxonomy-client";
|
|
6
6
|
|
|
7
7
|
function networkConversationStore(authorLabel: string): ConversationStore {
|
|
8
8
|
return {
|
|
@@ -10,6 +10,10 @@ function networkConversationStore(authorLabel: string): ConversationStore {
|
|
|
10
10
|
load: loadConversationMessages,
|
|
11
11
|
append: async ({ anchor, body, parentMessageId }) => (await appendConversationMessage({ anchor, body, parentMessageId })).message,
|
|
12
12
|
counts: loadReferenceConversationCounts,
|
|
13
|
+
viewer: loadConversationViewer,
|
|
14
|
+
authors: loadUserProfileNames,
|
|
15
|
+
edit: editConversationMessage,
|
|
16
|
+
remove: (messageId, moderate) => deleteConversationMessage(messageId, moderate),
|
|
13
17
|
};
|
|
14
18
|
}
|
|
15
19
|
|