@anthusai/papyrus 1.4.0-next.1 → 1.4.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/amplify/data/resource.js +134 -3
  2. package/amplify/data/resource.ts +145 -3
  3. package/amplify/data/schema.js +131 -0
  4. package/amplify/data/schema.ts +142 -0
  5. package/amplify/functions/category-action/handler.ts +272 -20
  6. package/amplify/functions/console-chat-responder/src/main.rs +1 -1
  7. package/amplify/functions/content-actions/handler.py +23 -0
  8. package/amplify/functions/content-actions/requirements.txt +1 -1
  9. package/amplify/functions/content-actions/resource.js +1 -1
  10. package/amplify/functions/content-actions/resource.ts +1 -1
  11. package/amplify/functions/renditions/requirements.txt +1 -1
  12. package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
  13. package/amplify/functions/shared/curation-delegation.ts +115 -0
  14. package/amplify/functions/shared/python-bundle.js +3 -3
  15. package/amplify/functions/shared/python-bundle.ts +3 -3
  16. package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
  17. package/amplify/functions/shared/slack-events.ts +2 -2
  18. package/amplify/publication/renditions-function.js +1 -1
  19. package/amplify/publication/renditions-function.ts +1 -1
  20. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
  21. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
  22. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
  23. package/app/[year]/[month]/[day]/page.tsx +3 -0
  24. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
  25. package/app/api/archive/editions/route.ts +2 -0
  26. package/app/api/auth/prospectus/accounts/route.ts +9 -0
  27. package/app/api/local-preview/derive/route.ts +28 -0
  28. package/app/api/local-preview/media/route.ts +25 -0
  29. package/app/archive/page.tsx +3 -0
  30. package/app/globals.css +28 -28
  31. package/app/layout.tsx +8 -5
  32. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  33. package/app/newsroom/[section]/page.tsx +3 -2
  34. package/app/newsroom/articles/page.tsx +8 -1
  35. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  36. package/app/newsroom/insights/page.tsx +4 -12
  37. package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
  38. package/app/page.tsx +30 -1
  39. package/components/administration-delegations-panel.tsx +156 -0
  40. package/components/curation-delegation-client.ts +104 -0
  41. package/components/item-stream-view.tsx +63 -0
  42. package/components/markus-ir-preview.tsx +47 -2
  43. package/components/news-desk-page.tsx +0 -1
  44. package/components/news-desk-taxonomy-client.ts +82 -242
  45. package/components/newsroom-article-editor.tsx +138 -40
  46. package/components/newsroom-assignment-detail.tsx +79 -0
  47. package/components/newsroom-assignments-view.tsx +1 -1
  48. package/components/newsroom-board-view.tsx +128 -51
  49. package/components/newsroom-newsprint-board.css +5 -46
  50. package/components/newsroom-ops-overview.tsx +27 -5
  51. package/components/newsroom-organizer-shell.tsx +26 -8
  52. package/components/newsroom-organizer.css +267 -12
  53. package/components/newsroom-references-view.tsx +100 -23
  54. package/components/newsroom-topics-view.tsx +1 -1
  55. package/components/priming-proof-page.tsx +17 -8
  56. package/components/prospectus-account-chooser.tsx +131 -0
  57. package/components/reference-cyclotron-status.tsx +5 -5
  58. package/components/reference-cyclotron-suggestion.tsx +143 -0
  59. package/components/reference-live-agreement.tsx +62 -0
  60. package/components/reference-section-suggestion.tsx +104 -0
  61. package/components/story-reporting-trail-client.ts +91 -0
  62. package/components/story-reporting-trail.tsx +115 -0
  63. package/components/topic-steering-workspace.tsx +245 -678
  64. package/components/ui/sheet.tsx +10 -4
  65. package/components/use-paged-content-item-versions.ts +151 -0
  66. package/components/wiki-concepts-view.tsx +153 -0
  67. package/components/wiki-index-client.ts +51 -0
  68. package/infra/amplify-app-shell.js +17 -3
  69. package/infra/amplify-app-shell.ts +18 -3
  70. package/infra/site-config.js +6 -1
  71. package/infra/site-config.ts +7 -1
  72. package/lib/approved-priming-proof-reports.ts +6 -3
  73. package/lib/approved-priming-proofs/papyrus.json +90 -0
  74. package/lib/blog-feature-solver.ts +1 -109
  75. package/lib/category-repository.ts +1 -1
  76. package/lib/content-actions-client.ts +3 -0
  77. package/lib/content-actions-demo.ts +55 -0
  78. package/lib/content-item-lifecycle.ts +34 -5
  79. package/lib/content-source-context.ts +9 -1
  80. package/lib/curation-delegation-helpers.ts +69 -0
  81. package/lib/cyclotron-suggestions.ts +263 -0
  82. package/lib/front-matter-fields.ts +42 -0
  83. package/lib/front-mosaic.ts +2 -1
  84. package/lib/graphql-content-repository.ts +42 -7
  85. package/lib/item-stream.ts +47 -0
  86. package/lib/layout-plan.ts +3 -2
  87. package/lib/layout-warnings.ts +8 -0
  88. package/lib/local-preview-section-fixtures.ts +72 -0
  89. package/lib/local-preview-seed.ts +196 -7
  90. package/lib/local-preview-tenant.ts +27 -0
  91. package/lib/newsroom-articles.ts +2 -0
  92. package/lib/newsroom-assignments.ts +14 -1
  93. package/lib/newsroom-board.ts +52 -7
  94. package/lib/newsroom-demo-dashboard.ts +1 -1
  95. package/lib/newsroom-forum-routes.ts +2 -43
  96. package/lib/newsroom-index-filters.ts +3 -40
  97. package/lib/newsroom-media-backend.ts +5 -0
  98. package/lib/newsroom-nav.ts +1 -3
  99. package/lib/newsroom-organizer-nav.ts +8 -14
  100. package/lib/newsroom-section-routing.ts +1 -1
  101. package/lib/newsroom-sections.ts +12 -0
  102. package/lib/papyrus-web-locations.ts +3 -24
  103. package/lib/priming-proof-report.ts +1 -0
  104. package/lib/prospectus-account-client.ts +90 -0
  105. package/lib/prospectus-sign-in-routes.ts +45 -0
  106. package/lib/prospectus-site-identity.ts +13 -1
  107. package/lib/prospectus-tenant.ts +22 -0
  108. package/lib/reader-static-params.ts +2 -1
  109. package/lib/relevance-decisions.ts +29 -0
  110. package/lib/reporting-story-budget.ts +2 -2
  111. package/lib/semantic-graph.ts +5 -5
  112. package/lib/site-brand.ts +20 -0
  113. package/lib/site-env.ts +5 -0
  114. package/lib/site-section-suggestions.ts +120 -0
  115. package/lib/site-stack.ts +1 -1
  116. package/lib/story-reporting-trail.ts +106 -0
  117. package/lib/tenant-keys.ts +32 -0
  118. package/lib/wiki.ts +56 -0
  119. package/package.json +34 -2
  120. package/renderers/pretext/rhythm-article-page.tsx +1 -18
  121. package/routes.manifest.json +34 -7
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
3
4
  import { isLocalPreviewEnabled } from "../lib/local-preview";
4
- import { createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
5
+ import { createLocalPreviewRealPublishedVersions, createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
5
6
  import { generateClient } from "aws-amplify/data";
6
7
  import { testEditorOverrideAllowed } from "../lib/test-editor-override";
7
8
  import { downloadData, getUrl } from "aws-amplify/storage";
@@ -41,7 +42,7 @@ import type {
41
42
  UserDirectoryEntry,
42
43
  } from "../lib/category-repository";
43
44
  import { relationTypeKey } from "../lib/semantic-graph";
44
- import { contentItemStageKeys, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
45
+ import { legacyStageKeysToLoad, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
45
46
  import { getContentItemStageDefinitions } from "../lib/site-brand";
46
47
  import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
47
48
  import { configureAmplifyClient } from "./amplify-client-provider";
@@ -690,16 +691,11 @@ type NewsroomSemanticRelationPageOptions = NewsroomPageOptions & {
690
691
 
691
692
  export type ForumThreadWithMessages = MessageThreadRecord & {
692
693
  messages: MessageRecord[];
693
- scope: "edition" | "section" | "insight";
694
+ scope: "edition" | "section";
694
695
  sectionKey?: string | null;
695
696
  sectionTitle?: string | null;
696
697
  };
697
698
 
698
- export type InsightForumThreadsPage = {
699
- threads: ForumThreadWithMessages[];
700
- nextToken: string | null;
701
- };
702
-
703
699
  export type EditionForumThreadsResult = {
704
700
  editionId: string;
705
701
  editionThreads: ForumThreadWithMessages[];
@@ -1097,6 +1093,27 @@ export async function loadEditorProcedureData(): Promise<{
1097
1093
  };
1098
1094
  }
1099
1095
 
1096
+ const UPDATE_ASSIGNMENT_DESK_MUTATION = `
1097
+ mutation UpdateAssignmentDesk($input: UpdateAssignmentInput!) {
1098
+ updateAssignment(input: $input) { id sectionId sectionKey }
1099
+ }
1100
+ `;
1101
+
1102
+ /** Puts an assignment on a desk's board, or takes it off every desk when `deskId` is null. */
1103
+ export async function saveNewsroomAssignmentDesk(assignmentId: string, deskId: string | null): Promise<void> {
1104
+ if (isLocalPreviewEnabled()) return;
1105
+ const dataClient = generateClient<Schema>();
1106
+ const graphClient = dataClient as unknown as {
1107
+ graphql: (options: Record<string, unknown>) => Promise<{ data?: Record<string, unknown> | null; errors?: unknown[] | null }>;
1108
+ };
1109
+ const response = await graphClient.graphql({
1110
+ query: UPDATE_ASSIGNMENT_DESK_MUTATION,
1111
+ variables: { input: { id: assignmentId, sectionId: deskId, sectionKey: deskId } },
1112
+ authMode: USER_POOL_AUTH_MODE,
1113
+ });
1114
+ assertNoGraphQLErrors(response.errors);
1115
+ }
1116
+
1100
1117
  export async function saveProcedureDefinitionRecord(input: Record<string, unknown>) {
1101
1118
  const dataClient = generateClient<Schema>();
1102
1119
  const graphClient = dataClient as unknown as {
@@ -1277,148 +1294,6 @@ export async function loadEditionForumThreads(options: {
1277
1294
  };
1278
1295
  }
1279
1296
 
1280
- export async function loadInsightForumThreads(options: {
1281
- domain?: string;
1282
- limit?: number;
1283
- nextToken?: string | null;
1284
- includeMessages?: boolean;
1285
- } = {}): Promise<InsightForumThreadsPage> {
1286
- const testMock = getTestEditorNewsroomMock();
1287
- if (testMock?.insightForumThreads) {
1288
- const domain = String(options.domain || "").trim();
1289
- let threads = testMock.insightForumThreads;
1290
- if (domain) threads = threads.filter((thread) => thread.sectionKey === domain);
1291
- return { threads, nextToken: null };
1292
- }
1293
-
1294
- const limit = Math.max(1, Math.min(options.limit ?? NEWSROOM_PAGE_LIMIT, 100));
1295
- const domain = String(options.domain || "").trim();
1296
- const includeMessages = options.includeMessages !== false;
1297
- type KindConnectionResponse = {
1298
- listMessagesByKindAndCreatedAt?: {
1299
- items?: Array<MessageRecord | null> | null;
1300
- nextToken?: string | null;
1301
- } | null;
1302
- };
1303
- const response: KindConnectionResponse = await runGraphql<KindConnectionResponse>(
1304
- LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY,
1305
- {
1306
- messageKind: "insight",
1307
- sortDirection: "DESC",
1308
- limit,
1309
- nextToken: options.nextToken ?? null,
1310
- },
1311
- );
1312
- const connection = response.listMessagesByKindAndCreatedAt;
1313
- const roots = (connection?.items ?? [])
1314
- .filter(Boolean)
1315
- .map((item) => normalizeMessageRecord(item as MessageRecord))
1316
- .filter(isInsightThreadRootMessage)
1317
- .filter((message) => !domain || message.messageDomain === domain);
1318
-
1319
- const threads = await Promise.all(roots.map(async (root) => {
1320
- const messages = includeMessages
1321
- ? await loadInsightThreadMessages(root.id, root)
1322
- : [root];
1323
- return buildInsightForumThread(root, messages);
1324
- }));
1325
-
1326
- return {
1327
- threads,
1328
- nextToken: connection?.nextToken ?? null,
1329
- };
1330
- }
1331
-
1332
- export async function loadInsightForumThreadById(
1333
- threadId: string,
1334
- options: { includeMessages?: boolean } = {},
1335
- ): Promise<ForumThreadWithMessages | null> {
1336
- const normalized = String(threadId || "").trim();
1337
- if (!normalized) return null;
1338
- const testMock = getTestEditorNewsroomMock();
1339
- if (testMock?.insightForumThreads) {
1340
- return testMock.insightForumThreads.find((thread) => thread.id === normalized) ?? null;
1341
- }
1342
- const root = await getMessageRecordById(normalized);
1343
- if (!root || !isInsightThreadRootMessage(root)) return null;
1344
- const includeMessages = options.includeMessages !== false;
1345
- const messages = includeMessages
1346
- ? await loadInsightThreadMessages(normalized, root)
1347
- : [root];
1348
- return buildInsightForumThread(root, messages);
1349
- }
1350
-
1351
- export async function appendInsightThreadReplyRecord(input: {
1352
- threadId: string;
1353
- summary: string;
1354
- content: string;
1355
- role?: string;
1356
- authorLabel?: string;
1357
- parentMessageId?: string | null;
1358
- metadata?: Record<string, unknown>;
1359
- }): Promise<MessageRecord> {
1360
- const threadId = String(input.threadId || "").trim();
1361
- const summary = String(input.summary || "").trim();
1362
- const content = String(input.content || "").trim();
1363
- if (!threadId) throw new Error("threadId is required");
1364
- if (!summary) throw new Error("summary is required");
1365
- if (!content) throw new Error("content is required");
1366
- const root = await getMessageRecordById(threadId);
1367
- if (!root || !isInsightThreadRootMessage(root)) throw new Error(`Insight thread not found: ${threadId}`);
1368
- const existing = await listMessagesByThreadId(threadId, 1000);
1369
- const nextSequence = Math.max(1, ...existing.map((message) => Number(message.sequenceNumber || 0))) + 1;
1370
- const now = new Date().toISOString();
1371
- const message: MessageRecord = {
1372
- id: `message-insight-reply-${safeForumId(threadId)}-${String(nextSequence).padStart(4, "0")}`,
1373
- messageKind: "insight_reply",
1374
- messageDomain: root.messageDomain ?? "knowledge",
1375
- status: "active",
1376
- summary,
1377
- source: "newsroom.insights",
1378
- authorLabel: input.authorLabel?.trim() || "editor",
1379
- threadId,
1380
- parentMessageId: input.parentMessageId?.trim() || null,
1381
- sequenceNumber: nextSequence,
1382
- role: input.role || "human",
1383
- messageType: "insight_forum_reply",
1384
- content,
1385
- metadata: input.metadata ?? {},
1386
- createdAt: now,
1387
- updatedAt: now,
1388
- newsroomFeedKey: "messages",
1389
- };
1390
- const created = await runGraphql<{ createMessage?: MessageRecord | null }>(
1391
- CREATE_MESSAGE_MUTATION,
1392
- { input: message },
1393
- );
1394
- return normalizeMessageRecord(created.createMessage ?? message);
1395
- }
1396
-
1397
- export async function deleteInsightThreadMessageRecord(input: {
1398
- threadId: string;
1399
- messageId: string;
1400
- }): Promise<MessageRecord> {
1401
- const threadId = String(input.threadId || "").trim();
1402
- const messageId = String(input.messageId || "").trim();
1403
- if (!threadId) throw new Error("threadId is required");
1404
- if (!messageId) throw new Error("messageId is required");
1405
- const existing = await listMessagesByThreadId(threadId, 1000);
1406
- const target = existing.find((message) => message.id === messageId);
1407
- if (!target) throw new Error(`Message not found in thread: ${messageId}`);
1408
- const now = new Date().toISOString();
1409
- const updatedMessage = await runGraphql<{ updateMessage?: MessageRecord | null }>(
1410
- UPDATE_MESSAGE_MUTATION,
1411
- {
1412
- input: {
1413
- id: target.id,
1414
- status: "deleted",
1415
- updatedAt: now,
1416
- },
1417
- },
1418
- );
1419
- return normalizeMessageRecord(updatedMessage.updateMessage ?? { ...target, status: "deleted", updatedAt: now });
1420
- }
1421
-
1422
1297
  export async function ensureEditionForumThreadRecord(input: {
1423
1298
  editionId: string;
1424
1299
  title?: string;
@@ -1682,14 +1557,6 @@ async function listMessagesByThreadId(threadId: string, limit = 400): Promise<Me
1682
1557
  return records;
1683
1558
  }
1684
1559
 
1685
- function isInsightThreadRootMessage(message: MessageRecord): boolean {
1686
- if (String(message.messageKind || "") !== "insight") return false;
1687
- const threadId = String(message.threadId || "").trim();
1688
- const sequenceNumber = Number(message.sequenceNumber || 0);
1689
- if (threadId && threadId !== message.id && sequenceNumber > 1) return false;
1690
- return true;
1691
- }
1692
-
1693
1560
  async function getMessageRecordById(messageId: string): Promise<MessageRecord | null> {
1694
1561
  const normalized = String(messageId || "").trim();
1695
1562
  if (!normalized) return null;
@@ -1701,66 +1568,6 @@ async function getMessageRecordById(messageId: string): Promise<MessageRecord |
1701
1568
  return message ? normalizeMessageRecord(message) : null;
1702
1569
  }
1703
1570
 
1704
- async function loadInsightThreadMessages(threadId: string, root: MessageRecord): Promise<MessageRecord[]> {
1705
- const threadMessages = await listMessagesByThreadId(threadId, 500);
1706
- const byId = new Map<string, MessageRecord>();
1707
- byId.set(root.id, root);
1708
- for (const message of threadMessages) byId.set(message.id, message);
1709
- const merged = [...byId.values()].sort(
1710
- (left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
1711
- );
1712
- return hydrateMessageRecordsContent(merged);
1713
- }
1714
-
1715
- function insightForumThreadTitle(root: MessageRecord): string {
1716
- const metadata = normalizeJsonValue(root.metadata);
1717
- if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
1718
- const explicit = String((metadata as Record<string, unknown>).insightTitle || "").trim();
1719
- if (explicit) return explicit;
1720
- }
1721
- const summary = String(root.summary || "").trim();
1722
- return summary || "Insight";
1723
- }
1724
-
1725
- function buildInsightForumThread(root: MessageRecord, messages: MessageRecord[]): ForumThreadWithMessages {
1726
- const activeMessages = messages.filter((message) => String(message.status || "active") === "active");
1727
- const sorted = [...activeMessages].sort(
1728
- (left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
1729
- );
1730
- const lastMessage = sorted.at(-1) ?? root;
1731
- const activityAt = lastMessage.updatedAt ?? lastMessage.createdAt ?? root.updatedAt ?? root.createdAt;
1732
- return {
1733
- id: root.id,
1734
- threadKind: "insight_forum",
1735
- status: "active",
1736
- title: insightForumThreadTitle(root),
1737
- summary: formatInsightForumDomainLabel(root.messageDomain),
1738
- primaryAnchorKind: "message",
1739
- primaryAnchorId: root.id,
1740
- primaryAnchorLineageId: root.id,
1741
- primaryAnchorKey: `message#${root.id}`,
1742
- createdByLabel: root.authorLabel ?? null,
1743
- messageCount: sorted.length,
1744
- lastMessageId: lastMessage.id,
1745
- lastMessageAt: activityAt,
1746
- metadata: normalizeJsonValue(root.metadata) as Record<string, unknown>,
1747
- createdAt: root.createdAt,
1748
- updatedAt: activityAt,
1749
- newsroomFeedKey: "messages",
1750
- messages: sorted.length ? sorted : [root],
1751
- scope: "insight",
1752
- sectionKey: root.messageDomain ?? null,
1753
- };
1754
- }
1755
-
1756
- function formatInsightForumDomainLabel(domain: string | null | undefined): string {
1757
- const value = String(domain || "").trim();
1758
- if (value === "assignment_work") return "Assignment research";
1759
- if (value === "knowledge") return "Reference knowledge";
1760
- if (!value) return "Insight";
1761
- return value.replace(/_/g, " ");
1762
- }
1763
-
1764
1571
  async function hydrateMessageRecordsContent(messages: MessageRecord[]): Promise<MessageRecord[]> {
1765
1572
  return Promise.all(messages.map(async (message) => {
1766
1573
  try {
@@ -2156,47 +1963,61 @@ export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPage
2156
1963
  newsroomFeedKey: "assignments",
2157
1964
  limit: options.limit,
2158
1965
  nextToken: options.nextToken,
2159
- matches: equalityMatcher({
1966
+ matches: (assignment) => isEditorialWorkAssignment(assignment) && (equalityMatcher<AssignmentRecord>({
2160
1967
  assignmentTypeKey: options.type,
2161
1968
  status: options.status,
2162
- }),
1969
+ })?.(assignment) ?? true),
2163
1970
  });
2164
1971
  }
2165
1972
 
2166
1973
  const CONTENT_ITEMS_BY_STAGE_QUERY = `
2167
1974
  query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
2168
1975
  listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
2169
- items { id lineageId versionNumber stage type status title headline slug section updatedAt }
1976
+ items { id lineageId versionNumber stage type status title headline deck metadata slug section updatedAt }
2170
1977
  nextToken
2171
1978
  }
2172
1979
  }
2173
1980
  `;
2174
1981
 
2175
- export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2176
- if (isLocalPreviewEnabled()) return createLocalPreviewContentItemVersions();
1982
+ export const CONTENT_ITEM_COLUMN_PAGE_LIMIT = 25;
1983
+
1984
+ export type ContentItemStagePage = {
1985
+ versions: ContentItemVersionRecord[];
1986
+ nextToken: string | null;
1987
+ };
1988
+
1989
+ /** One newest-updated-first page of the content items in one stage, so a board column can load as it is scrolled. */
1990
+ export async function loadNewsroomContentItemStagePage(
1991
+ stage: string,
1992
+ nextToken: string | null = null,
1993
+ limit: number = CONTENT_ITEM_COLUMN_PAGE_LIMIT,
1994
+ ): Promise<ContentItemStagePage> {
1995
+ if (isLocalPreviewEnabled()) {
1996
+ const versionsInStage = createLocalPreviewContentItemVersions().filter(version => version.stage === stage);
1997
+ const offset = nextToken ? Number(nextToken) : 0;
1998
+ const followingOffset = offset + limit;
1999
+ return {
2000
+ versions: versionsInStage.slice(offset, followingOffset),
2001
+ nextToken: followingOffset < versionsInStage.length ? String(followingOffset) : null,
2002
+ };
2003
+ }
2177
2004
  configureAmplifyClient();
2178
2005
  const client = generateClient<Schema>() as unknown as {
2179
2006
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
2180
2007
  };
2181
2008
  const field = "listItemsByStageAndUpdatedAt";
2182
- const versionsPerStage = await Promise.all(contentItemStageKeys(getContentItemStageDefinitions()).map(async (stage) => {
2183
- const versions: ContentItemVersionRecord[] = [];
2184
- let nextToken: string | null = null;
2185
- do {
2186
- const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
2187
- query: CONTENT_ITEMS_BY_STAGE_QUERY,
2188
- variables: { stage, sortDirection: "DESC", limit: NEWSROOM_PAGE_LIMIT, nextToken },
2189
- authMode: USER_POOL_AUTH_MODE,
2190
- });
2191
- assertNoGraphQLErrors(response.errors);
2192
- const connection = response.data?.[field];
2193
- if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
2194
- versions.push(...((connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[]));
2195
- nextToken = connection.nextToken ?? null;
2196
- } while (nextToken);
2197
- return versions;
2198
- }));
2199
- return versionsPerStage.flat();
2009
+ const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
2010
+ query: CONTENT_ITEMS_BY_STAGE_QUERY,
2011
+ variables: { stage, sortDirection: "DESC", limit, nextToken },
2012
+ authMode: USER_POOL_AUTH_MODE,
2013
+ });
2014
+ assertNoGraphQLErrors(response.errors);
2015
+ const connection = response.data?.[field];
2016
+ if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
2017
+ return {
2018
+ versions: (connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[],
2019
+ nextToken: connection.nextToken ?? null,
2020
+ };
2200
2021
  }
2201
2022
 
2202
2023
  const PUBLISHED_ITEMS_QUERY = `
@@ -2222,6 +2043,7 @@ type PublishedItemBoardRow = {
2222
2043
  };
2223
2044
 
2224
2045
  export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2046
+ if (isLocalPreviewEnabled()) return createLocalPreviewRealPublishedVersions();
2225
2047
  configureAmplifyClient();
2226
2048
  const client = generateClient<Schema>() as unknown as {
2227
2049
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
@@ -2675,7 +2497,6 @@ export function hasTestEditorOverride(): boolean {
2675
2497
 
2676
2498
  type TestEditorNewsroomMock = {
2677
2499
  forumThreadsByEdition?: Record<string, ForumThreadWithMessages[]>;
2678
- insightForumThreads?: ForumThreadWithMessages[];
2679
2500
  messages?: MessageRecord[];
2680
2501
  payloads?: Record<string, HydratedModelPayload[]>;
2681
2502
  referenceAttachments?: ReferenceAttachmentRecord[];
@@ -3800,6 +3621,25 @@ export async function loadCyclotronStatus(cyclotronId: string): Promise<import("
3800
3621
  return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3801
3622
  }
3802
3623
 
3624
+ /** The relevance cyclotron's latest cyclotron-shadow-agreement/v1 payload: suggestions compared with later human decisions. */
3625
+ export async function loadCyclotronShadowAgreement(cyclotronId: string): Promise<import("../lib/cyclotron-suggestions").ShadowAgreement | null> {
3626
+ const { latestShadowAgreement, shadowAgreementPayloadId } = await import("../lib/cyclotron-suggestions");
3627
+ const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", shadowAgreementPayloadId(cyclotronId), ["raw_payload"]);
3628
+ return latestShadowAgreement(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3629
+ }
3630
+
3631
+ /** The site cyclotron's latest per-Section agreement payload, as the Section sweep recorded it. */
3632
+ export async function loadSiteSectionAgreement(cyclotronId: string): Promise<import("../lib/site-section-suggestions").SiteSectionAgreement | null> {
3633
+ const { shadowAgreementPayloadId } = await import("../lib/cyclotron-suggestions");
3634
+ const { parseSiteSectionAgreement } = await import("../lib/site-section-suggestions");
3635
+ const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", shadowAgreementPayloadId(cyclotronId), ["raw_payload"]);
3636
+ const parsed = payloads
3637
+ .map((payload) => ({ agreement: parseSiteSectionAgreement(payload.json), updatedAt: payload.attachment.updatedAt }))
3638
+ .filter((entry) => entry.agreement !== null)
3639
+ .sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")));
3640
+ return parsed.length ? parsed[0].agreement : null;
3641
+ }
3642
+
3803
3643
  /** Ask the next decide-relevance sweep to set or clear a manual review rate. */
3804
3644
  export async function requestCyclotronReviewRate(
3805
3645
  cyclotronId: string,