@anthusai/papyrus 5.57.0 → 5.58.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/resource.js +25 -3
- package/amplify/data/resource.ts +25 -3
- package/amplify/data/schema.js +25 -3
- package/amplify/data/schema.ts +25 -3
- package/amplify/functions/category-action/handler.ts +51 -125
- package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
- package/components/conversation-thread.tsx +27 -12
- 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/needs-me-bodies.tsx +4 -3
- package/components/needs-me-feed.tsx +8 -14
- package/components/news-desk-taxonomy-client.ts +83 -14
- package/components/newsroom-organizer.css +24 -0
- package/components/notification-cards.tsx +5 -4
- package/components/reference-feed.tsx +60 -42
- package/components/reference-review-card.tsx +8 -134
- package/components/story-reporting-trail-client.ts +4 -3
- package/components/story-reporting-trail.tsx +8 -0
- package/components/topic-steering-workspace.tsx +40 -782
- package/components/use-reference-ratings.ts +11 -7
- package/lib/category-repository.ts +7 -0
- package/lib/conversation-feed.ts +96 -0
- package/lib/cyclotron-suggestions.ts +18 -36
- package/lib/local-preview-seed.ts +16 -27
- package/lib/reference-feed-keys.ts +69 -0
- package/lib/reference-policy.ts +83 -11
- package/lib/reference-review-details.ts +21 -57
- package/lib/relevance-decisions.ts +6 -2
- package/lib/story-reporting-trail.ts +28 -2
- package/package.json +9 -2
- package/routes.manifest.json +1 -1
- package/components/review-rating-grid.tsx +0 -190
package/amplify/data/resource.js
CHANGED
|
@@ -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,23 @@ 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
|
+
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1579
|
+
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1580
|
+
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
1581
|
+
// P(accept), written once, and never rewritten by a vote.
|
|
1582
|
+
feedStateKey: a.string(),
|
|
1583
|
+
feedArrivalAt: a.datetime(),
|
|
1584
|
+
predictedAcceptProbability: a.float(),
|
|
1585
|
+
predictionDecidedAt: a.datetime(),
|
|
1586
|
+
predictionSortKey: a.string(),
|
|
1574
1587
|
updatedAt: a.datetime(),
|
|
1575
1588
|
})
|
|
1576
1589
|
.secondaryIndexes((index) => [
|
|
@@ -1583,6 +1596,8 @@ const schema = a.schema({
|
|
|
1583
1596
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1584
1597
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1585
1598
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1599
|
+
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1600
|
+
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1586
1601
|
])
|
|
1587
1602
|
.authorization((allow) => [
|
|
1588
1603
|
allow.groups(categoryWriteGroups).to(["read"]),
|
|
@@ -1670,6 +1685,9 @@ const schema = a.schema({
|
|
|
1670
1685
|
authorSub: a.string(),
|
|
1671
1686
|
authorUserProfileId: a.id(),
|
|
1672
1687
|
authorLabel: a.string(),
|
|
1688
|
+
editedAt: a.datetime(),
|
|
1689
|
+
deletedAt: a.datetime(),
|
|
1690
|
+
deletedBySub: a.string(),
|
|
1673
1691
|
threadId: a.id(),
|
|
1674
1692
|
parentMessageId: a.id(),
|
|
1675
1693
|
sequenceNumber: a.integer(),
|
|
@@ -1704,8 +1722,12 @@ const schema = a.schema({
|
|
|
1704
1722
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1705
1723
|
])
|
|
1706
1724
|
.authorization((allow) => [
|
|
1707
|
-
|
|
1725
|
+
// One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
|
|
1726
|
+
allow.group("editor").to(categoryAppendOnlyOperations),
|
|
1727
|
+
allow.group(adminGroup).to(["read", "create", "delete"]),
|
|
1708
1728
|
allow.authenticated("identityPool").to(["read", "create", "update"]),
|
|
1729
|
+
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1730
|
+
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1709
1731
|
]),
|
|
1710
1732
|
MessageThread: a
|
|
1711
1733
|
.model({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -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,23 @@ 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
|
+
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1692
|
+
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1693
|
+
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
1694
|
+
// P(accept), written once, and never rewritten by a vote.
|
|
1695
|
+
feedStateKey: a.string(),
|
|
1696
|
+
feedArrivalAt: a.datetime(),
|
|
1697
|
+
predictedAcceptProbability: a.float(),
|
|
1698
|
+
predictionDecidedAt: a.datetime(),
|
|
1699
|
+
predictionSortKey: a.string(),
|
|
1687
1700
|
updatedAt: a.datetime(),
|
|
1688
1701
|
})
|
|
1689
1702
|
.secondaryIndexes((index) => [
|
|
@@ -1696,6 +1709,8 @@ const schema = a.schema({
|
|
|
1696
1709
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1697
1710
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1698
1711
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1712
|
+
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1713
|
+
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1699
1714
|
])
|
|
1700
1715
|
.authorization((allow) => [
|
|
1701
1716
|
allow.groups(categoryWriteGroups).to(["read"]),
|
|
@@ -1786,6 +1801,9 @@ const schema = a.schema({
|
|
|
1786
1801
|
authorSub: a.string(),
|
|
1787
1802
|
authorUserProfileId: a.id(),
|
|
1788
1803
|
authorLabel: a.string(),
|
|
1804
|
+
editedAt: a.datetime(),
|
|
1805
|
+
deletedAt: a.datetime(),
|
|
1806
|
+
deletedBySub: a.string(),
|
|
1789
1807
|
threadId: a.id(),
|
|
1790
1808
|
parentMessageId: a.id(),
|
|
1791
1809
|
sequenceNumber: a.integer(),
|
|
@@ -1820,8 +1838,12 @@ const schema = a.schema({
|
|
|
1820
1838
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1821
1839
|
])
|
|
1822
1840
|
.authorization((allow) => [
|
|
1823
|
-
|
|
1841
|
+
// One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
|
|
1842
|
+
allow.group("editor").to(categoryAppendOnlyOperations),
|
|
1843
|
+
allow.group(adminGroup).to(["read", "create", "delete"]),
|
|
1824
1844
|
allow.authenticated("identityPool").to(["read", "create", "update"]),
|
|
1845
|
+
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1846
|
+
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1825
1847
|
]),
|
|
1826
1848
|
|
|
1827
1849
|
MessageThread: a
|
package/amplify/data/schema.js
CHANGED
|
@@ -259,8 +259,6 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
259
259
|
referenceId: a.id().required(),
|
|
260
260
|
status: a.string().required(),
|
|
261
261
|
reasonCode: a.string(),
|
|
262
|
-
messageId: a.id(),
|
|
263
|
-
relationId: a.id(),
|
|
264
262
|
}),
|
|
265
263
|
ReferenceQualityActionResult: a.customType({
|
|
266
264
|
ok: a.boolean().required(),
|
|
@@ -1475,8 +1473,23 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1475
1473
|
curationStatusUpdatedAt: a.datetime(),
|
|
1476
1474
|
curationStatusUpdatedBy: a.string(),
|
|
1477
1475
|
curationStatusReason: a.string(),
|
|
1476
|
+
curationReasonCode: a.string(),
|
|
1477
|
+
curationActorKind: a.string(),
|
|
1478
|
+
curationSectionId: a.string(),
|
|
1479
|
+
curationShareable: a.boolean(),
|
|
1480
|
+
curationDelegation: a.json(),
|
|
1481
|
+
curationDecisionRelationId: a.string(),
|
|
1478
1482
|
newsroomFeedKey: a.string(),
|
|
1479
1483
|
reviewedFeedKey: a.string(),
|
|
1484
|
+
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1485
|
+
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1486
|
+
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
1487
|
+
// P(accept), written once, and never rewritten by a vote.
|
|
1488
|
+
feedStateKey: a.string(),
|
|
1489
|
+
feedArrivalAt: a.datetime(),
|
|
1490
|
+
predictedAcceptProbability: a.float(),
|
|
1491
|
+
predictionDecidedAt: a.datetime(),
|
|
1492
|
+
predictionSortKey: a.string(),
|
|
1480
1493
|
updatedAt: a.datetime(),
|
|
1481
1494
|
})
|
|
1482
1495
|
.secondaryIndexes((index) => [
|
|
@@ -1489,6 +1502,8 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1489
1502
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1490
1503
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1491
1504
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1505
|
+
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1506
|
+
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1492
1507
|
])
|
|
1493
1508
|
.authorization((allow) => [
|
|
1494
1509
|
allow.groups(categoryWriteGroups).to(["read"]),
|
|
@@ -1573,6 +1588,9 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1573
1588
|
authorSub: a.string(),
|
|
1574
1589
|
authorUserProfileId: a.id(),
|
|
1575
1590
|
authorLabel: a.string(),
|
|
1591
|
+
editedAt: a.datetime(),
|
|
1592
|
+
deletedAt: a.datetime(),
|
|
1593
|
+
deletedBySub: a.string(),
|
|
1576
1594
|
threadId: a.id(),
|
|
1577
1595
|
parentMessageId: a.id(),
|
|
1578
1596
|
sequenceNumber: a.integer(),
|
|
@@ -1607,7 +1625,11 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1607
1625
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1608
1626
|
])
|
|
1609
1627
|
.authorization((allow) => [
|
|
1610
|
-
|
|
1628
|
+
// One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
|
|
1629
|
+
allow.group("editor").to(categoryAppendOnlyOperations),
|
|
1630
|
+
allow.group(adminGroup).to(["read", "create", "delete"]),
|
|
1631
|
+
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1632
|
+
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1611
1633
|
]),
|
|
1612
1634
|
MessageThread: realtimeModel({
|
|
1613
1635
|
id: a.id().required(),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -285,8 +285,6 @@ const definitions = {
|
|
|
285
285
|
referenceId: a.id().required(),
|
|
286
286
|
status: a.string().required(),
|
|
287
287
|
reasonCode: a.string(),
|
|
288
|
-
messageId: a.id(),
|
|
289
|
-
relationId: a.id(),
|
|
290
288
|
}),
|
|
291
289
|
|
|
292
290
|
ReferenceQualityActionResult: a.customType({
|
|
@@ -1586,8 +1584,23 @@ const definitions = {
|
|
|
1586
1584
|
curationStatusUpdatedAt: a.datetime(),
|
|
1587
1585
|
curationStatusUpdatedBy: a.string(),
|
|
1588
1586
|
curationStatusReason: a.string(),
|
|
1587
|
+
curationReasonCode: a.string(),
|
|
1588
|
+
curationActorKind: a.string(),
|
|
1589
|
+
curationSectionId: a.string(),
|
|
1590
|
+
curationShareable: a.boolean(),
|
|
1591
|
+
curationDelegation: a.json(),
|
|
1592
|
+
curationDecisionRelationId: a.string(),
|
|
1589
1593
|
newsroomFeedKey: a.string(),
|
|
1590
1594
|
reviewedFeedKey: a.string(),
|
|
1595
|
+
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1596
|
+
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1597
|
+
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
1598
|
+
// P(accept), written once, and never rewritten by a vote.
|
|
1599
|
+
feedStateKey: a.string(),
|
|
1600
|
+
feedArrivalAt: a.datetime(),
|
|
1601
|
+
predictedAcceptProbability: a.float(),
|
|
1602
|
+
predictionDecidedAt: a.datetime(),
|
|
1603
|
+
predictionSortKey: a.string(),
|
|
1591
1604
|
updatedAt: a.datetime(),
|
|
1592
1605
|
})
|
|
1593
1606
|
.secondaryIndexes((index) => [
|
|
@@ -1600,6 +1613,8 @@ const definitions = {
|
|
|
1600
1613
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1601
1614
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1602
1615
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1616
|
+
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1617
|
+
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1603
1618
|
])
|
|
1604
1619
|
.authorization((allow) => [
|
|
1605
1620
|
allow.groups(categoryWriteGroups).to(["read"]),
|
|
@@ -1687,6 +1702,9 @@ const definitions = {
|
|
|
1687
1702
|
authorSub: a.string(),
|
|
1688
1703
|
authorUserProfileId: a.id(),
|
|
1689
1704
|
authorLabel: a.string(),
|
|
1705
|
+
editedAt: a.datetime(),
|
|
1706
|
+
deletedAt: a.datetime(),
|
|
1707
|
+
deletedBySub: a.string(),
|
|
1690
1708
|
threadId: a.id(),
|
|
1691
1709
|
parentMessageId: a.id(),
|
|
1692
1710
|
sequenceNumber: a.integer(),
|
|
@@ -1721,7 +1739,11 @@ const definitions = {
|
|
|
1721
1739
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1722
1740
|
])
|
|
1723
1741
|
.authorization((allow) => [
|
|
1724
|
-
|
|
1742
|
+
// One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
|
|
1743
|
+
allow.group("editor").to(categoryAppendOnlyOperations),
|
|
1744
|
+
allow.group(adminGroup).to(["read", "create", "delete"]),
|
|
1745
|
+
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1746
|
+
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1725
1747
|
]),
|
|
1726
1748
|
|
|
1727
1749
|
MessageThread: realtimeModel({
|
|
@@ -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
|
|