@anthusai/papyrus 5.43.0-next.45 → 5.43.0-next.47

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.
@@ -1571,6 +1571,15 @@ const schema = a.schema({
1571
1571
  curationStatusReason: a.string(),
1572
1572
  newsroomFeedKey: a.string(),
1573
1573
  reviewedFeedKey: a.string(),
1574
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1575
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1576
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1577
+ // P(accept), written once, and never rewritten by a vote.
1578
+ feedStateKey: a.string(),
1579
+ feedArrivalAt: a.datetime(),
1580
+ predictedAcceptProbability: a.float(),
1581
+ predictionDecidedAt: a.datetime(),
1582
+ predictionSortKey: a.string(),
1574
1583
  updatedAt: a.datetime(),
1575
1584
  })
1576
1585
  .secondaryIndexes((index) => [
@@ -1583,6 +1592,8 @@ const schema = a.schema({
1583
1592
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1584
1593
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1585
1594
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1595
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1596
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1586
1597
  ])
1587
1598
  .authorization((allow) => [
1588
1599
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1670,6 +1681,9 @@ const schema = a.schema({
1670
1681
  authorSub: a.string(),
1671
1682
  authorUserProfileId: a.id(),
1672
1683
  authorLabel: a.string(),
1684
+ editedAt: a.datetime(),
1685
+ deletedAt: a.datetime(),
1686
+ deletedBySub: a.string(),
1673
1687
  threadId: a.id(),
1674
1688
  parentMessageId: a.id(),
1675
1689
  sequenceNumber: a.integer(),
@@ -1704,8 +1718,12 @@ const schema = a.schema({
1704
1718
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1705
1719
  ])
1706
1720
  .authorization((allow) => [
1707
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1721
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1722
+ allow.group("editor").to(categoryAppendOnlyOperations),
1723
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1708
1724
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1725
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1726
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1709
1727
  ]),
1710
1728
  MessageThread: a
1711
1729
  .model({
@@ -1684,6 +1684,15 @@ const schema = a.schema({
1684
1684
  curationStatusReason: a.string(),
1685
1685
  newsroomFeedKey: a.string(),
1686
1686
  reviewedFeedKey: a.string(),
1687
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1688
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1689
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1690
+ // P(accept), written once, and never rewritten by a vote.
1691
+ feedStateKey: a.string(),
1692
+ feedArrivalAt: a.datetime(),
1693
+ predictedAcceptProbability: a.float(),
1694
+ predictionDecidedAt: a.datetime(),
1695
+ predictionSortKey: a.string(),
1687
1696
  updatedAt: a.datetime(),
1688
1697
  })
1689
1698
  .secondaryIndexes((index) => [
@@ -1696,6 +1705,8 @@ const schema = a.schema({
1696
1705
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1697
1706
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1698
1707
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1708
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1709
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1699
1710
  ])
1700
1711
  .authorization((allow) => [
1701
1712
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1786,6 +1797,9 @@ const schema = a.schema({
1786
1797
  authorSub: a.string(),
1787
1798
  authorUserProfileId: a.id(),
1788
1799
  authorLabel: a.string(),
1800
+ editedAt: a.datetime(),
1801
+ deletedAt: a.datetime(),
1802
+ deletedBySub: a.string(),
1789
1803
  threadId: a.id(),
1790
1804
  parentMessageId: a.id(),
1791
1805
  sequenceNumber: a.integer(),
@@ -1820,8 +1834,12 @@ const schema = a.schema({
1820
1834
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1821
1835
  ])
1822
1836
  .authorization((allow) => [
1823
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1837
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1838
+ allow.group("editor").to(categoryAppendOnlyOperations),
1839
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1824
1840
  allow.authenticated("identityPool").to(["read", "create", "update"]),
1841
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1842
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1825
1843
  ]),
1826
1844
 
1827
1845
  MessageThread: a
@@ -1477,6 +1477,15 @@ function buildBaseDefinitions(includeEditions) {
1477
1477
  curationStatusReason: a.string(),
1478
1478
  newsroomFeedKey: a.string(),
1479
1479
  reviewedFeedKey: a.string(),
1480
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1481
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1482
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1483
+ // P(accept), written once, and never rewritten by a vote.
1484
+ feedStateKey: a.string(),
1485
+ feedArrivalAt: a.datetime(),
1486
+ predictedAcceptProbability: a.float(),
1487
+ predictionDecidedAt: a.datetime(),
1488
+ predictionSortKey: a.string(),
1480
1489
  updatedAt: a.datetime(),
1481
1490
  })
1482
1491
  .secondaryIndexes((index) => [
@@ -1489,6 +1498,8 @@ function buildBaseDefinitions(includeEditions) {
1489
1498
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1490
1499
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1491
1500
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1501
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1502
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1492
1503
  ])
1493
1504
  .authorization((allow) => [
1494
1505
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1573,6 +1584,9 @@ function buildBaseDefinitions(includeEditions) {
1573
1584
  authorSub: a.string(),
1574
1585
  authorUserProfileId: a.id(),
1575
1586
  authorLabel: a.string(),
1587
+ editedAt: a.datetime(),
1588
+ deletedAt: a.datetime(),
1589
+ deletedBySub: a.string(),
1576
1590
  threadId: a.id(),
1577
1591
  parentMessageId: a.id(),
1578
1592
  sequenceNumber: a.integer(),
@@ -1607,7 +1621,11 @@ function buildBaseDefinitions(includeEditions) {
1607
1621
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1608
1622
  ])
1609
1623
  .authorization((allow) => [
1610
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1624
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1625
+ allow.group("editor").to(categoryAppendOnlyOperations),
1626
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1627
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1628
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1611
1629
  ]),
1612
1630
  MessageThread: realtimeModel({
1613
1631
  id: a.id().required(),
@@ -1588,6 +1588,15 @@ const definitions = {
1588
1588
  curationStatusReason: a.string(),
1589
1589
  newsroomFeedKey: a.string(),
1590
1590
  reviewedFeedKey: a.string(),
1591
+ // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
1592
+ // "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
1593
+ // `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
1594
+ // P(accept), written once, and never rewritten by a vote.
1595
+ feedStateKey: a.string(),
1596
+ feedArrivalAt: a.datetime(),
1597
+ predictedAcceptProbability: a.float(),
1598
+ predictionDecidedAt: a.datetime(),
1599
+ predictionSortKey: a.string(),
1591
1600
  updatedAt: a.datetime(),
1592
1601
  })
1593
1602
  .secondaryIndexes((index) => [
@@ -1600,6 +1609,8 @@ const definitions = {
1600
1609
  index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
1601
1610
  index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
1602
1611
  index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
1612
+ index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
1613
+ index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
1603
1614
  ])
1604
1615
  .authorization((allow) => [
1605
1616
  allow.groups(categoryWriteGroups).to(["read"]),
@@ -1687,6 +1698,9 @@ const definitions = {
1687
1698
  authorSub: a.string(),
1688
1699
  authorUserProfileId: a.id(),
1689
1700
  authorLabel: a.string(),
1701
+ editedAt: a.datetime(),
1702
+ deletedAt: a.datetime(),
1703
+ deletedBySub: a.string(),
1690
1704
  threadId: a.id(),
1691
1705
  parentMessageId: a.id(),
1692
1706
  sequenceNumber: a.integer(),
@@ -1721,7 +1735,11 @@ const definitions = {
1721
1735
  index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
1722
1736
  ])
1723
1737
  .authorization((allow) => [
1724
- allow.groups(categoryWriteGroups).to(categoryAppendOnlyOperations),
1738
+ // One rule per group (a group may appear once per model): editors read and add; admins also delete, for moderation.
1739
+ allow.group("editor").to(categoryAppendOnlyOperations),
1740
+ allow.group(adminGroup).to(["read", "create", "delete"]),
1741
+ // Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
1742
+ allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
1725
1743
  ]),
1726
1744
 
1727
1745
  MessageThread: realtimeModel({
@@ -25,6 +25,7 @@ import {
25
25
  type DelegationStore,
26
26
  } from "../shared/curation-delegation-flow";
27
27
  import { rejectionReasonCodeForWrittenReason } from "../../../lib/reference-policy";
28
+ import { referenceFeedKeys, referenceFeedStateKey } from "../../../lib/reference-feed-keys";
28
29
  import { claimGroups } from "../../../lib/staff-groups";
29
30
 
30
31
  type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
@@ -256,6 +257,8 @@ async function reviewReferenceCuration(
256
257
  curationStatusReason: note,
257
258
  newsroomFeedKey: reference.newsroomFeedKey ?? "references",
258
259
  reviewedFeedKey: reviewedFeedKeyForStatus(nextStatus),
260
+ // A vote moves the Reference between the unrated and rated feeds; its original prediction and arrival stay as they were.
261
+ feedStateKey: referenceFeedStateKey({ ...reference, curationStatus: nextStatus }),
259
262
  updatedAt: now,
260
263
  }),
261
264
  "update Reference curation status",
@@ -642,6 +645,8 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
642
645
  curationStatusReason: note,
643
646
  newsroomFeedKey: reference.newsroomFeedKey ?? "references",
644
647
  reviewedFeedKey: reviewedFeedKeyForStatus(nextStatus),
648
+ // A vote moves the Reference between the unrated and rated feeds; its original prediction and arrival stay as they were.
649
+ feedStateKey: referenceFeedStateKey({ ...reference, curationStatus: nextStatus }),
645
650
  updatedAt: now,
646
651
  }),
647
652
  "update Reference quality status",
@@ -818,6 +823,7 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
818
823
  client.models.Reference.update({
819
824
  id: normalizeRequiredString(current.id, "reference.id"),
820
825
  versionState: "superseded",
826
+ feedStateKey: null, // a superseded version is in no feed
821
827
  updatedAt: now,
822
828
  }),
823
829
  "supersede current Reference version",
@@ -857,6 +863,19 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
857
863
  curationStatusReason: normalizeOptionalString(current.curationStatusReason),
858
864
  newsroomFeedKey: normalizeOptionalString(current.newsroomFeedKey) ?? "references",
859
865
  reviewedFeedKey: normalizeOptionalString(current.reviewedFeedKey) ?? reviewedFeedKeyForStatus(curationStatus),
866
+ predictedAcceptProbability: typeof current.predictedAcceptProbability === "number" ? current.predictedAcceptProbability : null,
867
+ predictionDecidedAt: normalizeOptionalString(current.predictionDecidedAt),
868
+ ...referenceFeedKeys({
869
+ id: nextReferenceId,
870
+ corpusId: targetCorpusId,
871
+ curationStatus,
872
+ versionState: "current",
873
+ predictedAcceptProbability: typeof current.predictedAcceptProbability === "number" ? current.predictedAcceptProbability : null,
874
+ createdAt: normalizeOptionalString(current.createdAt) ?? now,
875
+ importedAt: normalizeOptionalString(current.importedAt),
876
+ retrievedAt: normalizeOptionalString(current.retrievedAt),
877
+ sourcePublishedAt: normalizeOptionalString(current.sourcePublishedAt),
878
+ }),
860
879
  updatedAt: now,
861
880
  }),
862
881
  "create moved Reference version",
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The keys the References page reads through (docs/reference-feed-indexes.md). The Python twin is
3
+ * src/papyrus_content/reference_feed_keys.py; scripts/fixtures/reference-feed-key-vectors.json holds the vectors both test.
4
+ *
5
+ * `feedStateKey` is "<corpusId>#unrated" (pending) or "<corpusId>#rated" (accepted or rejected); a Reference that is not the current
6
+ * version, or is archived, has none and is in neither feed. `predictionSortKey` is the Cyclotron's ORIGINAL P(accept), then arrival, then
7
+ * id; a Reference nobody predicted sorts as 0.5. A vote changes `feedStateKey` and nothing else here.
8
+ */
9
+ import { normalizeReferenceCurationStatus } from "./reference-policy";
10
+
11
+ export const UNDECIDED_PROBABILITY = 0.5;
12
+ const EPOCH = "1970-01-01T00:00:00.000Z";
13
+ const ARRIVAL_FIELDS = ["createdAt", "importedAt", "retrievedAt", "sourcePublishedAt", "versionCreatedAt"] as const;
14
+
15
+ export type ReferenceFeedKeyInput = {
16
+ id: string;
17
+ corpusId?: string | null;
18
+ curationStatus?: string | null;
19
+ versionState?: string | null;
20
+ predictedAcceptProbability?: number | null;
21
+ createdAt?: string | null;
22
+ importedAt?: string | null;
23
+ retrievedAt?: string | null;
24
+ sourcePublishedAt?: string | null;
25
+ versionCreatedAt?: string | null;
26
+ };
27
+
28
+ export type ReferenceFeedKeys = {
29
+ feedStateKey: string | null;
30
+ feedArrivalAt: string;
31
+ predictionSortKey: string;
32
+ };
33
+
34
+ function instant(value: string | null | undefined): string | null {
35
+ if (!value) return null;
36
+ const parsed = Date.parse(value);
37
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
38
+ }
39
+
40
+ export function referenceFeedArrivalAt(reference: ReferenceFeedKeyInput): string {
41
+ for (const field of ARRIVAL_FIELDS) {
42
+ const found = instant(reference[field]);
43
+ if (found) return found;
44
+ }
45
+ return EPOCH;
46
+ }
47
+
48
+ export function referenceFeedStateKey(reference: ReferenceFeedKeyInput): string | null {
49
+ if (reference.versionState && reference.versionState !== "current") return null;
50
+ if (!reference.corpusId) return null;
51
+ const status = normalizeReferenceCurationStatus(reference.curationStatus);
52
+ if (status === "archived") return null;
53
+ return `${reference.corpusId}#${status === "pending" ? "unrated" : "rated"}`;
54
+ }
55
+
56
+ export function predictionSortKeyFor(probability: number | null | undefined, arrival: string, referenceId: string): string {
57
+ const chosen = probability === null || probability === undefined ? UNDECIDED_PROBABILITY : Math.min(1, Math.max(0, probability));
58
+ return `${chosen.toFixed(6)}#${arrival}#${referenceId}`;
59
+ }
60
+
61
+ /** The three feed keys for a Reference as it stands. `probability` overrides the one stored on it. */
62
+ export function referenceFeedKeys(reference: ReferenceFeedKeyInput, probability?: number | null): ReferenceFeedKeys {
63
+ const arrival = referenceFeedArrivalAt(reference);
64
+ return {
65
+ feedStateKey: referenceFeedStateKey(reference),
66
+ feedArrivalAt: arrival,
67
+ predictionSortKey: predictionSortKeyFor(probability ?? reference.predictedAcceptProbability, arrival, reference.id),
68
+ };
69
+ }
package/package.json CHANGED
@@ -354,6 +354,7 @@
354
354
  "./lib/reference-capture": "./lib/reference-capture.ts",
355
355
  "./lib/reference-display": "./lib/reference-display.ts",
356
356
  "./lib/reference-feed-cache": "./lib/reference-feed-cache.ts",
357
+ "./lib/reference-feed-keys": "./lib/reference-feed-keys.ts",
357
358
  "./lib/reference-feed-ranking": "./lib/reference-feed-ranking.ts",
358
359
  "./lib/reference-policy": "./lib/reference-policy.ts",
359
360
  "./lib/reference-preview": "./lib/reference-preview.ts",
@@ -516,7 +517,7 @@
516
517
  "optional": true
517
518
  }
518
519
  },
519
- "version": "5.43.0-next.45",
520
+ "version": "5.43.0-next.47",
520
521
  "dependencies": {
521
522
  "@aws-amplify/adapter-nextjs": "^1.7.3",
522
523
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.43.0-next.45",
2
+ "version": "5.43.0-next.47",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",