@anthusai/papyrus 1.3.0 → 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 (141) 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/model-attachment-upload/handler.ts +1 -0
  12. package/amplify/functions/renditions/requirements.txt +1 -1
  13. package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
  14. package/amplify/functions/shared/curation-delegation.ts +115 -0
  15. package/amplify/functions/shared/python-bundle.js +3 -3
  16. package/amplify/functions/shared/python-bundle.ts +3 -3
  17. package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
  18. package/amplify/functions/shared/slack-events.ts +2 -2
  19. package/amplify/publication/renditions-function.js +1 -1
  20. package/amplify/publication/renditions-function.ts +1 -1
  21. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
  22. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
  23. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
  24. package/app/[year]/[month]/[day]/page.tsx +3 -0
  25. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
  26. package/app/api/archive/editions/route.ts +2 -0
  27. package/app/api/auth/prospectus/accounts/route.ts +9 -0
  28. package/app/api/local-preview/derive/route.ts +28 -0
  29. package/app/api/local-preview/media/route.ts +25 -0
  30. package/app/archive/page.tsx +3 -0
  31. package/app/globals.css +28 -28
  32. package/app/layout.tsx +8 -5
  33. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  34. package/app/newsroom/[section]/page.tsx +8 -1
  35. package/app/newsroom/articles/page.tsx +10 -0
  36. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  37. package/app/newsroom/insights/page.tsx +4 -12
  38. package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
  39. package/app/page.tsx +30 -1
  40. package/components/administration-delegations-panel.tsx +156 -0
  41. package/components/curation-delegation-client.ts +104 -0
  42. package/components/item-stream-view.tsx +63 -0
  43. package/components/markus-ir-preview.tsx +47 -2
  44. package/components/news-desk-client-provider.tsx +17 -2
  45. package/components/news-desk-page.tsx +0 -1
  46. package/components/news-desk-taxonomy-client.ts +154 -240
  47. package/components/newspaper.tsx +4 -4
  48. package/components/newsroom-article-editor.tsx +138 -40
  49. package/components/newsroom-assignment-detail.tsx +79 -0
  50. package/components/newsroom-assignments-view.tsx +1 -1
  51. package/components/newsroom-board-view.tsx +153 -43
  52. package/components/newsroom-client-shell.tsx +8 -1
  53. package/components/newsroom-newsprint-board.css +5 -46
  54. package/components/newsroom-ops-overview.tsx +27 -5
  55. package/components/newsroom-organizer-shell.tsx +36 -13
  56. package/components/newsroom-organizer.css +269 -13
  57. package/components/newsroom-references-view.tsx +118 -23
  58. package/components/newsroom-topics-view.tsx +1 -1
  59. package/components/priming-proof-page.tsx +17 -8
  60. package/components/prospectus-account-chooser.tsx +131 -0
  61. package/components/reference-cyclotron-priming.tsx +85 -0
  62. package/components/reference-cyclotron-status.tsx +5 -5
  63. package/components/reference-cyclotron-suggestion.tsx +143 -0
  64. package/components/reference-live-agreement.tsx +62 -0
  65. package/components/reference-section-suggestion.tsx +104 -0
  66. package/components/story-reporting-trail-client.ts +91 -0
  67. package/components/story-reporting-trail.tsx +115 -0
  68. package/components/topic-steering-workspace.tsx +280 -679
  69. package/components/ui/sheet.tsx +10 -4
  70. package/components/use-paged-content-item-versions.ts +151 -0
  71. package/components/wiki-concepts-view.tsx +153 -0
  72. package/components/wiki-index-client.ts +51 -0
  73. package/infra/amplify-app-shell.js +17 -3
  74. package/infra/amplify-app-shell.ts +18 -3
  75. package/infra/site-config.js +6 -1
  76. package/infra/site-config.ts +7 -1
  77. package/lib/approved-priming-proof-reports.ts +6 -3
  78. package/lib/approved-priming-proofs/papyrus.json +90 -0
  79. package/lib/article-flow.ts +95 -0
  80. package/lib/article-frame-solver.ts +519 -0
  81. package/lib/blog-feature-solver.ts +1 -109
  82. package/lib/category-repository.ts +1 -1
  83. package/lib/chrome-text.ts +504 -0
  84. package/lib/content-actions-client.ts +3 -0
  85. package/lib/content-actions-demo.ts +55 -0
  86. package/lib/content-item-lifecycle.ts +34 -5
  87. package/lib/content-source-context.ts +9 -1
  88. package/lib/curation-delegation-helpers.ts +69 -0
  89. package/lib/cyclotron-suggestions.ts +263 -0
  90. package/lib/front-matter-fields.ts +42 -0
  91. package/lib/front-mosaic.ts +1133 -0
  92. package/lib/furniture.ts +385 -0
  93. package/lib/graphql-content-repository.ts +42 -7
  94. package/lib/item-stream.ts +47 -0
  95. package/lib/labels.ts +37 -0
  96. package/lib/layout-config.ts +237 -0
  97. package/lib/layout-plan.ts +40 -11
  98. package/lib/layout-scenarios.ts +21 -11
  99. package/lib/layout-types.ts +457 -0
  100. package/lib/layout-warnings.ts +8 -0
  101. package/lib/local-preview-section-fixtures.ts +72 -0
  102. package/lib/local-preview-seed.ts +286 -0
  103. package/lib/local-preview-tenant.ts +27 -0
  104. package/lib/local-preview.ts +3 -0
  105. package/lib/newspaper-layout.ts +17 -3410
  106. package/lib/newsroom-articles.ts +2 -0
  107. package/lib/newsroom-assignments.ts +14 -1
  108. package/lib/newsroom-board.ts +99 -47
  109. package/lib/newsroom-demo-dashboard.ts +1 -1
  110. package/lib/newsroom-forum-routes.ts +2 -43
  111. package/lib/newsroom-index-filters.ts +3 -40
  112. package/lib/newsroom-media-backend.ts +5 -0
  113. package/lib/newsroom-nav.ts +1 -3
  114. package/lib/newsroom-organizer-nav.ts +9 -16
  115. package/lib/newsroom-section-routing.ts +1 -1
  116. package/lib/newsroom-sections.ts +12 -0
  117. package/lib/papyrus-web-locations.ts +3 -24
  118. package/lib/pretext-layout.ts +22 -0
  119. package/lib/priming-proof-report.ts +1 -0
  120. package/lib/prospectus-account-client.ts +90 -0
  121. package/lib/prospectus-sign-in-routes.ts +45 -0
  122. package/lib/prospectus-site-identity.ts +13 -1
  123. package/lib/prospectus-tenant.ts +22 -0
  124. package/lib/reader-static-params.ts +2 -1
  125. package/lib/relevance-decisions.ts +29 -0
  126. package/lib/relevance-status.ts +73 -0
  127. package/lib/reporting-story-budget.ts +2 -2
  128. package/lib/semantic-graph.ts +5 -5
  129. package/lib/site-brand.ts +28 -1
  130. package/lib/site-env.ts +5 -0
  131. package/lib/site-section-suggestions.ts +120 -0
  132. package/lib/site-stack.ts +1 -1
  133. package/lib/story-reporting-trail.ts +106 -0
  134. package/lib/tenant-keys.ts +32 -0
  135. package/lib/text-columns.ts +251 -0
  136. package/lib/wiki.ts +56 -0
  137. package/package.json +46 -3
  138. package/renderers/pretext/presentation-shell.tsx +1 -1
  139. package/renderers/pretext/rhythm-article-page.tsx +1 -18
  140. package/routes.manifest.json +34 -15
  141. package/app/newsroom/work/page.tsx +0 -10
@@ -3,6 +3,7 @@
3
3
  import { Hub } from "aws-amplify/utils";
4
4
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
5
  import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
6
+ import type { CategorySteeringDashboard } from "../lib/category-repository";
6
7
  import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
7
8
  import {
8
9
  loadEditorAssignmentsData,
@@ -46,7 +47,12 @@ const NewsDeskClientContext = createContext<NewsDeskClientContextValue | null>(n
46
47
  export function NewsDeskClientProvider({
47
48
  children,
48
49
  devSandboxEditorAuth = null,
49
- }: Readonly<{ children: React.ReactNode; devSandboxEditorAuth?: DevSandboxEditorAuth | null }>) {
50
+ localPreviewDashboard = null,
51
+ }: Readonly<{
52
+ children: React.ReactNode;
53
+ devSandboxEditorAuth?: DevSandboxEditorAuth | null;
54
+ localPreviewDashboard?: CategorySteeringDashboard | null;
55
+ }>) {
50
56
  const [shell, setShell] = useState<NewsDeskShellState>(() => createInitialNewsDeskShellState());
51
57
  const [searchTransition, setSearchTransition] = useState<NewsDeskSearchTransition | null>(null);
52
58
  const shellRef = useRef(shell);
@@ -58,6 +64,15 @@ export function NewsDeskClientProvider({
58
64
 
59
65
  const refreshDashboard = useCallback(async () => {
60
66
  const sequence = ++bootstrapSequenceRef.current;
67
+ if (localPreviewDashboard) {
68
+ setShell((current) => resolveDashboardReady(
69
+ current,
70
+ localPreviewDashboard,
71
+ { status: "signedIn", label: "Local preview editor", email: null },
72
+ new Date().toISOString(),
73
+ ));
74
+ return;
75
+ }
61
76
  setShell((current) => beginAccessCheck(current));
62
77
 
63
78
  try {
@@ -101,7 +116,7 @@ export function NewsDeskClientProvider({
101
116
  error instanceof Error ? error.message : "Could not load Newsroom data.",
102
117
  ));
103
118
  }
104
- }, [devSandboxEditorAuth]);
119
+ }, [devSandboxEditorAuth, localPreviewDashboard]);
105
120
 
106
121
  const refreshAssignments = useCallback(async () => {
107
122
  try {
@@ -86,7 +86,6 @@ function parseRouteSelection(tab: NewsDeskTab, selectionPath: string[] | null |
86
86
  if (segments[0] === "forum" && segments[1]) return { forumThread: segments[1] };
87
87
  return { message: segments[0] ?? null };
88
88
  }
89
- if (tab === "insights") return { forumThread: segments[0] ?? null };
90
89
  if (tab === "assignments") return { assignment: segments[0] ?? null };
91
90
  if (tab === "overview" && segments[0] === "forum" && segments[1]) return { forumThread: segments[1] };
92
91
  return {};
@@ -1,5 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
4
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
5
+ import { createLocalPreviewRealPublishedVersions, createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
3
6
  import { generateClient } from "aws-amplify/data";
4
7
  import { testEditorOverrideAllowed } from "../lib/test-editor-override";
5
8
  import { downloadData, getUrl } from "aws-amplify/storage";
@@ -39,7 +42,7 @@ import type {
39
42
  UserDirectoryEntry,
40
43
  } from "../lib/category-repository";
41
44
  import { relationTypeKey } from "../lib/semantic-graph";
42
- import { contentItemStageKeys, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
45
+ import { legacyStageKeysToLoad, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
43
46
  import { getContentItemStageDefinitions } from "../lib/site-brand";
44
47
  import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
45
48
  import { configureAmplifyClient } from "./amplify-client-provider";
@@ -688,16 +691,11 @@ type NewsroomSemanticRelationPageOptions = NewsroomPageOptions & {
688
691
 
689
692
  export type ForumThreadWithMessages = MessageThreadRecord & {
690
693
  messages: MessageRecord[];
691
- scope: "edition" | "section" | "insight";
694
+ scope: "edition" | "section";
692
695
  sectionKey?: string | null;
693
696
  sectionTitle?: string | null;
694
697
  };
695
698
 
696
- export type InsightForumThreadsPage = {
697
- threads: ForumThreadWithMessages[];
698
- nextToken: string | null;
699
- };
700
-
701
699
  export type EditionForumThreadsResult = {
702
700
  editionId: string;
703
701
  editionThreads: ForumThreadWithMessages[];
@@ -1095,6 +1093,27 @@ export async function loadEditorProcedureData(): Promise<{
1095
1093
  };
1096
1094
  }
1097
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
+
1098
1117
  export async function saveProcedureDefinitionRecord(input: Record<string, unknown>) {
1099
1118
  const dataClient = generateClient<Schema>();
1100
1119
  const graphClient = dataClient as unknown as {
@@ -1275,148 +1294,6 @@ export async function loadEditionForumThreads(options: {
1275
1294
  };
1276
1295
  }
1277
1296
 
1278
- export async function loadInsightForumThreads(options: {
1279
- domain?: string;
1280
- limit?: number;
1281
- nextToken?: string | null;
1282
- includeMessages?: boolean;
1283
- } = {}): Promise<InsightForumThreadsPage> {
1284
- const testMock = getTestEditorNewsroomMock();
1285
- if (testMock?.insightForumThreads) {
1286
- const domain = String(options.domain || "").trim();
1287
- let threads = testMock.insightForumThreads;
1288
- if (domain) threads = threads.filter((thread) => thread.sectionKey === domain);
1289
- return { threads, nextToken: null };
1290
- }
1291
-
1292
- const limit = Math.max(1, Math.min(options.limit ?? NEWSROOM_PAGE_LIMIT, 100));
1293
- const domain = String(options.domain || "").trim();
1294
- const includeMessages = options.includeMessages !== false;
1295
- type KindConnectionResponse = {
1296
- listMessagesByKindAndCreatedAt?: {
1297
- items?: Array<MessageRecord | null> | null;
1298
- nextToken?: string | null;
1299
- } | null;
1300
- };
1301
- const response: KindConnectionResponse = await runGraphql<KindConnectionResponse>(
1302
- LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY,
1303
- {
1304
- messageKind: "insight",
1305
- sortDirection: "DESC",
1306
- limit,
1307
- nextToken: options.nextToken ?? null,
1308
- },
1309
- );
1310
- const connection = response.listMessagesByKindAndCreatedAt;
1311
- const roots = (connection?.items ?? [])
1312
- .filter(Boolean)
1313
- .map((item) => normalizeMessageRecord(item as MessageRecord))
1314
- .filter(isInsightThreadRootMessage)
1315
- .filter((message) => !domain || message.messageDomain === domain);
1316
-
1317
- const threads = await Promise.all(roots.map(async (root) => {
1318
- const messages = includeMessages
1319
- ? await loadInsightThreadMessages(root.id, root)
1320
- : [root];
1321
- return buildInsightForumThread(root, messages);
1322
- }));
1323
-
1324
- return {
1325
- threads,
1326
- nextToken: connection?.nextToken ?? null,
1327
- };
1328
- }
1329
-
1330
- export async function loadInsightForumThreadById(
1331
- threadId: string,
1332
- options: { includeMessages?: boolean } = {},
1333
- ): Promise<ForumThreadWithMessages | null> {
1334
- const normalized = String(threadId || "").trim();
1335
- if (!normalized) return null;
1336
- const testMock = getTestEditorNewsroomMock();
1337
- if (testMock?.insightForumThreads) {
1338
- return testMock.insightForumThreads.find((thread) => thread.id === normalized) ?? null;
1339
- }
1340
- const root = await getMessageRecordById(normalized);
1341
- if (!root || !isInsightThreadRootMessage(root)) return null;
1342
- const includeMessages = options.includeMessages !== false;
1343
- const messages = includeMessages
1344
- ? await loadInsightThreadMessages(normalized, root)
1345
- : [root];
1346
- return buildInsightForumThread(root, messages);
1347
- }
1348
-
1349
- export async function appendInsightThreadReplyRecord(input: {
1350
- threadId: string;
1351
- summary: string;
1352
- content: string;
1353
- role?: string;
1354
- authorLabel?: string;
1355
- parentMessageId?: string | null;
1356
- metadata?: Record<string, unknown>;
1357
- }): Promise<MessageRecord> {
1358
- const threadId = String(input.threadId || "").trim();
1359
- const summary = String(input.summary || "").trim();
1360
- const content = String(input.content || "").trim();
1361
- if (!threadId) throw new Error("threadId is required");
1362
- if (!summary) throw new Error("summary is required");
1363
- if (!content) throw new Error("content is required");
1364
- const root = await getMessageRecordById(threadId);
1365
- if (!root || !isInsightThreadRootMessage(root)) throw new Error(`Insight thread not found: ${threadId}`);
1366
- const existing = await listMessagesByThreadId(threadId, 1000);
1367
- const nextSequence = Math.max(1, ...existing.map((message) => Number(message.sequenceNumber || 0))) + 1;
1368
- const now = new Date().toISOString();
1369
- const message: MessageRecord = {
1370
- id: `message-insight-reply-${safeForumId(threadId)}-${String(nextSequence).padStart(4, "0")}`,
1371
- messageKind: "insight_reply",
1372
- messageDomain: root.messageDomain ?? "knowledge",
1373
- status: "active",
1374
- summary,
1375
- source: "newsroom.insights",
1376
- authorLabel: input.authorLabel?.trim() || "editor",
1377
- threadId,
1378
- parentMessageId: input.parentMessageId?.trim() || null,
1379
- sequenceNumber: nextSequence,
1380
- role: input.role || "human",
1381
- messageType: "insight_forum_reply",
1382
- content,
1383
- metadata: input.metadata ?? {},
1384
- createdAt: now,
1385
- updatedAt: now,
1386
- newsroomFeedKey: "messages",
1387
- };
1388
- const created = await runGraphql<{ createMessage?: MessageRecord | null }>(
1389
- CREATE_MESSAGE_MUTATION,
1390
- { input: message },
1391
- );
1392
- return normalizeMessageRecord(created.createMessage ?? message);
1393
- }
1394
-
1395
- export async function deleteInsightThreadMessageRecord(input: {
1396
- threadId: string;
1397
- messageId: string;
1398
- }): Promise<MessageRecord> {
1399
- const threadId = String(input.threadId || "").trim();
1400
- const messageId = String(input.messageId || "").trim();
1401
- if (!threadId) throw new Error("threadId is required");
1402
- if (!messageId) throw new Error("messageId is required");
1403
- const existing = await listMessagesByThreadId(threadId, 1000);
1404
- const target = existing.find((message) => message.id === messageId);
1405
- if (!target) throw new Error(`Message not found in thread: ${messageId}`);
1406
- const now = new Date().toISOString();
1407
- const updatedMessage = await runGraphql<{ updateMessage?: MessageRecord | null }>(
1408
- UPDATE_MESSAGE_MUTATION,
1409
- {
1410
- input: {
1411
- id: target.id,
1412
- status: "deleted",
1413
- updatedAt: now,
1414
- },
1415
- },
1416
- );
1417
- return normalizeMessageRecord(updatedMessage.updateMessage ?? { ...target, status: "deleted", updatedAt: now });
1418
- }
1419
-
1420
1297
  export async function ensureEditionForumThreadRecord(input: {
1421
1298
  editionId: string;
1422
1299
  title?: string;
@@ -1680,14 +1557,6 @@ async function listMessagesByThreadId(threadId: string, limit = 400): Promise<Me
1680
1557
  return records;
1681
1558
  }
1682
1559
 
1683
- function isInsightThreadRootMessage(message: MessageRecord): boolean {
1684
- if (String(message.messageKind || "") !== "insight") return false;
1685
- const threadId = String(message.threadId || "").trim();
1686
- const sequenceNumber = Number(message.sequenceNumber || 0);
1687
- if (threadId && threadId !== message.id && sequenceNumber > 1) return false;
1688
- return true;
1689
- }
1690
-
1691
1560
  async function getMessageRecordById(messageId: string): Promise<MessageRecord | null> {
1692
1561
  const normalized = String(messageId || "").trim();
1693
1562
  if (!normalized) return null;
@@ -1699,66 +1568,6 @@ async function getMessageRecordById(messageId: string): Promise<MessageRecord |
1699
1568
  return message ? normalizeMessageRecord(message) : null;
1700
1569
  }
1701
1570
 
1702
- async function loadInsightThreadMessages(threadId: string, root: MessageRecord): Promise<MessageRecord[]> {
1703
- const threadMessages = await listMessagesByThreadId(threadId, 500);
1704
- const byId = new Map<string, MessageRecord>();
1705
- byId.set(root.id, root);
1706
- for (const message of threadMessages) byId.set(message.id, message);
1707
- const merged = [...byId.values()].sort(
1708
- (left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
1709
- );
1710
- return hydrateMessageRecordsContent(merged);
1711
- }
1712
-
1713
- function insightForumThreadTitle(root: MessageRecord): string {
1714
- const metadata = normalizeJsonValue(root.metadata);
1715
- if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
1716
- const explicit = String((metadata as Record<string, unknown>).insightTitle || "").trim();
1717
- if (explicit) return explicit;
1718
- }
1719
- const summary = String(root.summary || "").trim();
1720
- return summary || "Insight";
1721
- }
1722
-
1723
- function buildInsightForumThread(root: MessageRecord, messages: MessageRecord[]): ForumThreadWithMessages {
1724
- const activeMessages = messages.filter((message) => String(message.status || "active") === "active");
1725
- const sorted = [...activeMessages].sort(
1726
- (left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
1727
- );
1728
- const lastMessage = sorted.at(-1) ?? root;
1729
- const activityAt = lastMessage.updatedAt ?? lastMessage.createdAt ?? root.updatedAt ?? root.createdAt;
1730
- return {
1731
- id: root.id,
1732
- threadKind: "insight_forum",
1733
- status: "active",
1734
- title: insightForumThreadTitle(root),
1735
- summary: formatInsightForumDomainLabel(root.messageDomain),
1736
- primaryAnchorKind: "message",
1737
- primaryAnchorId: root.id,
1738
- primaryAnchorLineageId: root.id,
1739
- primaryAnchorKey: `message#${root.id}`,
1740
- createdByLabel: root.authorLabel ?? null,
1741
- messageCount: sorted.length,
1742
- lastMessageId: lastMessage.id,
1743
- lastMessageAt: activityAt,
1744
- metadata: normalizeJsonValue(root.metadata) as Record<string, unknown>,
1745
- createdAt: root.createdAt,
1746
- updatedAt: activityAt,
1747
- newsroomFeedKey: "messages",
1748
- messages: sorted.length ? sorted : [root],
1749
- scope: "insight",
1750
- sectionKey: root.messageDomain ?? null,
1751
- };
1752
- }
1753
-
1754
- function formatInsightForumDomainLabel(domain: string | null | undefined): string {
1755
- const value = String(domain || "").trim();
1756
- if (value === "assignment_work") return "Assignment research";
1757
- if (value === "knowledge") return "Reference knowledge";
1758
- if (!value) return "Insight";
1759
- return value.replace(/_/g, " ");
1760
- }
1761
-
1762
1571
  async function hydrateMessageRecordsContent(messages: MessageRecord[]): Promise<MessageRecord[]> {
1763
1572
  return Promise.all(messages.map(async (message) => {
1764
1573
  try {
@@ -1863,6 +1672,7 @@ export function normalizeNewsroomReferencePageOrder(value?: string | null): News
1863
1672
  }
1864
1673
 
1865
1674
  export async function loadNewsroomReferencePage(options: NewsroomReferencePageOptions = {}): Promise<NewsroomRecordPage<ReferenceRecord>> {
1675
+ if (isLocalPreviewEnabled()) return { items: createLocalPreviewReferences(), nextToken: null, hasMore: false };
1866
1676
  configureAmplifyClient();
1867
1677
  const order = normalizeNewsroomReferencePageOrder(options.order);
1868
1678
  if (order === "imported") {
@@ -2146,52 +1956,68 @@ export async function loadEditorOverviewEditionData(): Promise<{
2146
1956
  }
2147
1957
 
2148
1958
  export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPageOptions = {}): Promise<NewsroomRecordPage<AssignmentRecord>> {
1959
+ if (isLocalPreviewEnabled()) return { items: createLocalPreviewAssignments(), nextToken: null, hasMore: false };
2149
1960
  return loadNewsroomFeedPage<AssignmentRecord>({
2150
1961
  query: NEWSROOM_ASSIGNMENT_FEED_QUERY,
2151
1962
  field: "listAssignmentsByNewsroomFeedAndCreatedAt",
2152
1963
  newsroomFeedKey: "assignments",
2153
1964
  limit: options.limit,
2154
1965
  nextToken: options.nextToken,
2155
- matches: equalityMatcher({
1966
+ matches: (assignment) => isEditorialWorkAssignment(assignment) && (equalityMatcher<AssignmentRecord>({
2156
1967
  assignmentTypeKey: options.type,
2157
1968
  status: options.status,
2158
- }),
1969
+ })?.(assignment) ?? true),
2159
1970
  });
2160
1971
  }
2161
1972
 
2162
1973
  const CONTENT_ITEMS_BY_STAGE_QUERY = `
2163
1974
  query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
2164
1975
  listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
2165
- 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 }
2166
1977
  nextToken
2167
1978
  }
2168
1979
  }
2169
1980
  `;
2170
1981
 
2171
- export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
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
+ }
2172
2004
  configureAmplifyClient();
2173
2005
  const client = generateClient<Schema>() as unknown as {
2174
2006
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
2175
2007
  };
2176
2008
  const field = "listItemsByStageAndUpdatedAt";
2177
- const versionsPerStage = await Promise.all(contentItemStageKeys(getContentItemStageDefinitions()).map(async (stage) => {
2178
- const versions: ContentItemVersionRecord[] = [];
2179
- let nextToken: string | null = null;
2180
- do {
2181
- const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
2182
- query: CONTENT_ITEMS_BY_STAGE_QUERY,
2183
- variables: { stage, sortDirection: "DESC", limit: NEWSROOM_PAGE_LIMIT, nextToken },
2184
- authMode: USER_POOL_AUTH_MODE,
2185
- });
2186
- assertNoGraphQLErrors(response.errors);
2187
- const connection = response.data?.[field];
2188
- if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
2189
- versions.push(...((connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[]));
2190
- nextToken = connection.nextToken ?? null;
2191
- } while (nextToken);
2192
- return versions;
2193
- }));
2194
- 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
+ };
2195
2021
  }
2196
2022
 
2197
2023
  const PUBLISHED_ITEMS_QUERY = `
@@ -2217,6 +2043,7 @@ type PublishedItemBoardRow = {
2217
2043
  };
2218
2044
 
2219
2045
  export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2046
+ if (isLocalPreviewEnabled()) return createLocalPreviewRealPublishedVersions();
2220
2047
  configureAmplifyClient();
2221
2048
  const client = generateClient<Schema>() as unknown as {
2222
2049
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
@@ -2670,7 +2497,6 @@ export function hasTestEditorOverride(): boolean {
2670
2497
 
2671
2498
  type TestEditorNewsroomMock = {
2672
2499
  forumThreadsByEdition?: Record<string, ForumThreadWithMessages[]>;
2673
- insightForumThreads?: ForumThreadWithMessages[];
2674
2500
  messages?: MessageRecord[];
2675
2501
  payloads?: Record<string, HydratedModelPayload[]>;
2676
2502
  referenceAttachments?: ReferenceAttachmentRecord[];
@@ -3795,6 +3621,25 @@ export async function loadCyclotronStatus(cyclotronId: string): Promise<import("
3795
3621
  return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3796
3622
  }
3797
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
+
3798
3643
  /** Ask the next decide-relevance sweep to set or clear a manual review rate. */
3799
3644
  export async function requestCyclotronReviewRate(
3800
3645
  cyclotronId: string,
@@ -3826,3 +3671,72 @@ export async function requestCyclotronReviewRate(
3826
3671
  content: JSON.stringify(request),
3827
3672
  });
3828
3673
  }
3674
+
3675
+ const LIST_PRIMING_REQUEST_PAYLOADS_QUERY = `
3676
+ query ListKnowledgeRawPayloadsByOwnerAndKind($ownerId: ID!, $payloadKind: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) {
3677
+ listKnowledgeRawPayloadsByOwnerAndKind(ownerId: $ownerId, payloadKind: $payloadKind, limit: $limit, nextToken: $nextToken) {
3678
+ items { id }
3679
+ nextToken
3680
+ }
3681
+ }
3682
+ `;
3683
+
3684
+ /** Ask the next decide-relevance sweep to prime the running cyclotron with one pasted URL. */
3685
+ export async function requestCyclotronPriming(
3686
+ cyclotronId: string,
3687
+ request: import("../lib/relevance-status").PrimingRequest,
3688
+ ): Promise<void> {
3689
+ const { primingRequestPayloadId, PRIMING_REQUEST_KIND, safeId } = await import("../lib/relevance-status");
3690
+ const id = primingRequestPayloadId(cyclotronId, request.requestId);
3691
+ await runGraphql(CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION, {
3692
+ input: {
3693
+ id,
3694
+ ownerType: "cyclotron",
3695
+ ownerId: safeId(cyclotronId),
3696
+ payloadKind: PRIMING_REQUEST_KIND,
3697
+ createdAt: request.requestedAt,
3698
+ updatedAt: request.requestedAt,
3699
+ },
3700
+ });
3701
+ await uploadModelPayloadForOwner({
3702
+ ownerKind: "knowledgeRawPayload",
3703
+ ownerId: id,
3704
+ ownerLineageId: id,
3705
+ role: "raw_payload",
3706
+ sortKey: "priming-request",
3707
+ filename: "priming-request.json",
3708
+ mediaType: "application/json",
3709
+ content: JSON.stringify(request),
3710
+ });
3711
+ }
3712
+
3713
+ /** The saved priming requests of a cyclotron, newest first, each with the sweep's outcome once there is one. */
3714
+ export async function loadCyclotronPrimingRequests(
3715
+ cyclotronId: string,
3716
+ ): Promise<import("../lib/relevance-status").PrimingRequestView[]> {
3717
+ const { PRIMING_OUTCOME_ROLE, PRIMING_REQUEST_KIND, isPrimingOutcome, safeId } = await import("../lib/relevance-status");
3718
+ const ids: string[] = [];
3719
+ let nextToken: string | null = null;
3720
+ do {
3721
+ const page: { listKnowledgeRawPayloadsByOwnerAndKind?: { items?: Array<{ id: string } | null>; nextToken?: string | null } | null } =
3722
+ await runGraphql(LIST_PRIMING_REQUEST_PAYLOADS_QUERY, {
3723
+ ownerId: safeId(cyclotronId), payloadKind: { eq: PRIMING_REQUEST_KIND }, limit: 100, nextToken,
3724
+ });
3725
+ const connection = page.listKnowledgeRawPayloadsByOwnerAndKind;
3726
+ ids.push(...(connection?.items ?? []).filter((item): item is { id: string } => Boolean(item)).map((item) => item.id));
3727
+ nextToken = connection?.nextToken ?? null;
3728
+ } while (nextToken);
3729
+ const views = await Promise.all(ids.map(async (id) => {
3730
+ const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", id, ["raw_payload", PRIMING_OUTCOME_ROLE]);
3731
+ const request = payloads.find((payload) => payload.attachment.role === "raw_payload")?.json;
3732
+ const outcome = payloads.find((payload) => payload.attachment.role === PRIMING_OUTCOME_ROLE)?.json;
3733
+ if (!request || typeof request !== "object") return null;
3734
+ return {
3735
+ request: request as import("../lib/relevance-status").PrimingRequest,
3736
+ outcome: isPrimingOutcome(outcome) ? outcome : null,
3737
+ };
3738
+ }));
3739
+ return views
3740
+ .filter((view): view is import("../lib/relevance-status").PrimingRequestView => view !== null)
3741
+ .sort((left, right) => right.request.requestedAt.localeCompare(left.request.requestedAt));
3742
+ }
@@ -1139,10 +1139,10 @@ function FrontStoryBlock({
1139
1139
  <div className="jump-line">
1140
1140
  {block.jumpTargetPage ? (
1141
1141
  disableLinks ? (
1142
- <span>{block.jumpLabel ?? `SEE MORE ON A${block.jumpTargetPage}`}</span>
1142
+ <span>{block.jumpLabel}</span>
1143
1143
  ) : (
1144
1144
  <Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
1145
- {block.jumpLabel ?? `SEE MORE ON A${block.jumpTargetPage}`}
1145
+ {block.jumpLabel}
1146
1146
  </Link>
1147
1147
  )
1148
1148
  ) : (
@@ -1178,10 +1178,10 @@ function FrontStoryBlock({
1178
1178
  <div className="jump-line">
1179
1179
  {block.jumpTargetPage ? (
1180
1180
  disableLinks ? (
1181
- <span>{block.jumpLabel ?? `SEE MORE ON A${block.jumpTargetPage}`}</span>
1181
+ <span>{block.jumpLabel}</span>
1182
1182
  ) : (
1183
1183
  <Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
1184
- {block.jumpLabel ?? `SEE MORE ON A${block.jumpTargetPage}`}
1184
+ {block.jumpLabel}
1185
1185
  </Link>
1186
1186
  )
1187
1187
  ) : (