@anthusai/papyrus 1.10.0 → 1.12.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.
Files changed (79) hide show
  1. package/amplify/data/resource.js +70 -3
  2. package/amplify/data/resource.ts +75 -3
  3. package/amplify/data/schema.js +67 -0
  4. package/amplify/data/schema.ts +72 -0
  5. package/amplify/functions/category-action/handler.ts +190 -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.ts +67 -0
  13. package/amplify/functions/shared/reference-curation-metadata.ts +20 -0
  14. package/amplify/functions/shared/slack-events.ts +2 -2
  15. package/amplify/publication/renditions-function.js +1 -1
  16. package/amplify/publication/renditions-function.ts +1 -1
  17. package/app/api/local-preview/derive/route.ts +28 -0
  18. package/app/api/local-preview/media/route.ts +25 -0
  19. package/app/globals.css +28 -28
  20. package/app/layout.tsx +8 -5
  21. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  22. package/app/newsroom/[section]/page.tsx +0 -1
  23. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  24. package/app/newsroom/insights/page.tsx +4 -12
  25. package/components/administration-delegations-panel.tsx +204 -0
  26. package/components/board-card-excerpts.tsx +45 -0
  27. package/components/curation-delegation-client.ts +81 -0
  28. package/components/markus-ir-preview.tsx +47 -2
  29. package/components/news-desk-page.tsx +0 -1
  30. package/components/news-desk-taxonomy-client.ts +32 -220
  31. package/components/newsroom-article-editor.tsx +135 -39
  32. package/components/newsroom-assignment-detail.tsx +35 -1
  33. package/components/newsroom-board-view.tsx +29 -7
  34. package/components/newsroom-newsprint-board.css +9 -2
  35. package/components/newsroom-ops-overview.tsx +27 -5
  36. package/components/newsroom-organizer-shell.tsx +25 -7
  37. package/components/newsroom-organizer.css +218 -4
  38. package/components/newsroom-references-view.tsx +56 -21
  39. package/components/priming-proof-page.tsx +17 -8
  40. package/components/prospectus-account-chooser.tsx +98 -0
  41. package/components/reference-cyclotron-status.tsx +5 -5
  42. package/components/reference-cyclotron-suggestion.tsx +143 -0
  43. package/components/reference-live-agreement.tsx +62 -0
  44. package/components/story-reporting-trail-client.ts +91 -0
  45. package/components/story-reporting-trail.tsx +115 -0
  46. package/components/topic-steering-workspace.tsx +203 -674
  47. package/components/ui/sheet.tsx +1 -0
  48. package/components/wiki-concepts-view.tsx +153 -0
  49. package/components/wiki-index-client.ts +51 -0
  50. package/lib/content-actions-client.ts +3 -0
  51. package/lib/content-actions-demo.ts +55 -3
  52. package/lib/content-item-lifecycle.ts +4 -2
  53. package/lib/content-source-context.ts +9 -1
  54. package/lib/curation-delegation-helpers.ts +59 -0
  55. package/lib/cyclotron-suggestions.ts +263 -0
  56. package/lib/front-matter-fields.ts +42 -0
  57. package/lib/graphql-content-repository.ts +42 -7
  58. package/lib/local-preview-seed.ts +174 -3
  59. package/lib/local-preview-tenant.ts +27 -0
  60. package/lib/newsroom-articles.ts +2 -0
  61. package/lib/newsroom-board.ts +4 -0
  62. package/lib/newsroom-forum-routes.ts +2 -43
  63. package/lib/newsroom-index-filters.ts +3 -40
  64. package/lib/newsroom-media-backend.ts +5 -0
  65. package/lib/newsroom-nav.ts +1 -3
  66. package/lib/newsroom-organizer-nav.ts +5 -12
  67. package/lib/newsroom-section-routing.ts +1 -1
  68. package/lib/newsroom-sections.ts +12 -0
  69. package/lib/papyrus-web-locations.ts +3 -24
  70. package/lib/priming-proof-report.ts +1 -0
  71. package/lib/prospectus-tenant.ts +22 -0
  72. package/lib/relevance-decisions.ts +29 -0
  73. package/lib/semantic-graph.ts +5 -5
  74. package/lib/site-brand.ts +9 -0
  75. package/lib/story-reporting-trail.ts +106 -0
  76. package/lib/tenant-keys.ts +32 -0
  77. package/lib/wiki.ts +56 -0
  78. package/package.json +21 -1
  79. package/routes.manifest.json +23 -7
@@ -21,6 +21,8 @@ type PreviewContext = {
21
21
  sidecar: PreviewSidecar;
22
22
  citationNumbers: Map<string, number>;
23
23
  imageUrls: Record<string, string>;
24
+ dynamicDirectives: Record<string, (attributes: Record<string, unknown>) => ReactNode>;
25
+ renderLink?: (href: string, children: ReactNode) => ReactNode | null;
24
26
  };
25
27
 
26
28
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -62,7 +64,7 @@ function imagePlaceholder(token: string, context: PreviewContext): ReactNode {
62
64
  // eslint-disable-next-line @next/next/no-img-element
63
65
  <img
64
66
  alt={alt}
65
- className="my-3 h-auto max-w-full rounded-lg"
67
+ className="my-3 h-auto max-w-full"
66
68
  data-markus-preview-image-loaded
67
69
  loading="lazy"
68
70
  src={url}
@@ -112,7 +114,9 @@ function renderInlines(inlines: MarkusInline[], context: PreviewContext): ReactN
112
114
  return <s key={index}>{renderInlines(node.children, context)}</s>;
113
115
  case "code_span":
114
116
  return <code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]" key={index}>{node.code}</code>;
115
- case "link":
117
+ case "link": {
118
+ const custom = node.href ? context.renderLink?.(node.href, renderInlines(node.children, context)) : null;
119
+ if (custom) return <span key={index}>{custom}</span>;
116
120
  return node.href ? (
117
121
  <a className="text-primary underline" href={node.href} key={index} rel="noopener noreferrer" target="_blank">
118
122
  {renderInlines(node.children, context)}
@@ -120,6 +124,7 @@ function renderInlines(inlines: MarkusInline[], context: PreviewContext): ReactN
120
124
  ) : (
121
125
  <span key={index}>{renderInlines(node.children, context)}</span>
122
126
  );
127
+ }
123
128
  case "image":
124
129
  return (
125
130
  <span className="rounded border border-dashed border-border px-1 text-xs text-muted-foreground" key={index}>
@@ -228,6 +233,36 @@ function renderBlock(block: MarkusBlock, index: number, context: PreviewContext)
228
233
  }
229
234
  }
230
235
 
236
+ function normalizedFigureSource(src: string): string {
237
+ return src.replace(/^(\.\.?\/)+/, "");
238
+ }
239
+
240
+ function renderFigure(directive: MarkusDirective, index: number, context: PreviewContext): ReactNode {
241
+ const attribute = (name: string) => typeof directive.attributes[name] === "string" ? String(directive.attributes[name]) : "";
242
+ const src = attribute("src");
243
+ const alt = attribute("alt");
244
+ const caption = attribute("caption");
245
+ const credit = attribute("credit");
246
+ const url = context.imageUrls[src] ?? context.imageUrls[normalizedFigureSource(src)];
247
+ return (
248
+ <figure className="my-4" data-markus-preview-directive="figure" key={index}>
249
+ {url ? (
250
+ // eslint-disable-next-line @next/next/no-img-element
251
+ <img alt={alt} className="h-auto max-w-full" data-markus-preview-image-loaded loading="lazy" src={url} />
252
+ ) : (
253
+ <div className="border border-dashed border-border bg-muted/30 px-3 py-6 text-center text-xs text-muted-foreground" data-markus-preview-image>
254
+ {`Figure: ${src}${alt ? ` (${alt})` : ""}`}
255
+ </div>
256
+ )}
257
+ {caption || credit ? (
258
+ <figcaption className="mt-1 text-sm text-muted-foreground">
259
+ {caption}{caption && credit ? " " : ""}{credit ? <span className="text-xs">{credit}</span> : null}
260
+ </figcaption>
261
+ ) : null}
262
+ </figure>
263
+ );
264
+ }
265
+
231
266
  function renderDirective(directive: MarkusDirective, index: number, context: PreviewContext): ReactNode {
232
267
  if (directive.name === "pull-quote") {
233
268
  const attribution = directive.attributes.attribution;
@@ -244,6 +279,9 @@ function renderDirective(directive: MarkusDirective, index: number, context: Pre
244
279
  </blockquote>
245
280
  );
246
281
  }
282
+ if (directive.name === "figure") return renderFigure(directive, index, context);
283
+ const dynamicRenderer = context.dynamicDirectives[directive.name];
284
+ if (dynamicRenderer) return <div data-markus-preview-directive={directive.name} key={index}>{dynamicRenderer(directive.attributes)}</div>;
247
285
  const attributeEntries = Object.entries(directive.attributes).filter(([, value]) => value !== null && value !== "");
248
286
  return (
249
287
  <section
@@ -275,9 +313,14 @@ function renderBlocks(nodes: MarkusNode[], context: PreviewContext): ReactNode[]
275
313
  export function MarkusIrPreview({
276
314
  bodyIr,
277
315
  imageUrls = {},
316
+ dynamicDirectives = {},
317
+ renderLink,
278
318
  }: {
279
319
  bodyIr: unknown;
280
320
  imageUrls?: Record<string, string>;
321
+ /** Live components for leaf directives whose content the host app computes, keyed by directive name. */
322
+ dynamicDirectives?: PreviewContext["dynamicDirectives"];
323
+ renderLink?: PreviewContext["renderLink"];
281
324
  }) {
282
325
  try {
283
326
  const envelope = typeof bodyIr === "string" ? JSON.parse(bodyIr) : bodyIr;
@@ -288,6 +331,8 @@ export function MarkusIrPreview({
288
331
  sidecar,
289
332
  citationNumbers: numberCitationsByFirstAppearance(envelope.document, sidecar),
290
333
  imageUrls,
334
+ dynamicDirectives,
335
+ renderLink,
291
336
  };
292
337
  return <div data-markus-preview>{renderBlocks(document.children, context)}</div>;
293
338
  } catch (error) {
@@ -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 {};
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
4
4
  import { isLocalPreviewEnabled } from "../lib/local-preview";
5
- import { createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
5
+ import { createLocalPreviewRealPublishedVersions, createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
6
6
  import { generateClient } from "aws-amplify/data";
7
7
  import { testEditorOverrideAllowed } from "../lib/test-editor-override";
8
8
  import { downloadData, getUrl } from "aws-amplify/storage";
@@ -691,16 +691,11 @@ type NewsroomSemanticRelationPageOptions = NewsroomPageOptions & {
691
691
 
692
692
  export type ForumThreadWithMessages = MessageThreadRecord & {
693
693
  messages: MessageRecord[];
694
- scope: "edition" | "section" | "insight";
694
+ scope: "edition" | "section";
695
695
  sectionKey?: string | null;
696
696
  sectionTitle?: string | null;
697
697
  };
698
698
 
699
- export type InsightForumThreadsPage = {
700
- threads: ForumThreadWithMessages[];
701
- nextToken: string | null;
702
- };
703
-
704
699
  export type EditionForumThreadsResult = {
705
700
  editionId: string;
706
701
  editionThreads: ForumThreadWithMessages[];
@@ -1098,6 +1093,27 @@ export async function loadEditorProcedureData(): Promise<{
1098
1093
  };
1099
1094
  }
1100
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
+
1101
1117
  export async function saveProcedureDefinitionRecord(input: Record<string, unknown>) {
1102
1118
  const dataClient = generateClient<Schema>();
1103
1119
  const graphClient = dataClient as unknown as {
@@ -1278,148 +1294,6 @@ export async function loadEditionForumThreads(options: {
1278
1294
  };
1279
1295
  }
1280
1296
 
1281
- export async function loadInsightForumThreads(options: {
1282
- domain?: string;
1283
- limit?: number;
1284
- nextToken?: string | null;
1285
- includeMessages?: boolean;
1286
- } = {}): Promise<InsightForumThreadsPage> {
1287
- const testMock = getTestEditorNewsroomMock();
1288
- if (testMock?.insightForumThreads) {
1289
- const domain = String(options.domain || "").trim();
1290
- let threads = testMock.insightForumThreads;
1291
- if (domain) threads = threads.filter((thread) => thread.sectionKey === domain);
1292
- return { threads, nextToken: null };
1293
- }
1294
-
1295
- const limit = Math.max(1, Math.min(options.limit ?? NEWSROOM_PAGE_LIMIT, 100));
1296
- const domain = String(options.domain || "").trim();
1297
- const includeMessages = options.includeMessages !== false;
1298
- type KindConnectionResponse = {
1299
- listMessagesByKindAndCreatedAt?: {
1300
- items?: Array<MessageRecord | null> | null;
1301
- nextToken?: string | null;
1302
- } | null;
1303
- };
1304
- const response: KindConnectionResponse = await runGraphql<KindConnectionResponse>(
1305
- LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY,
1306
- {
1307
- messageKind: "insight",
1308
- sortDirection: "DESC",
1309
- limit,
1310
- nextToken: options.nextToken ?? null,
1311
- },
1312
- );
1313
- const connection = response.listMessagesByKindAndCreatedAt;
1314
- const roots = (connection?.items ?? [])
1315
- .filter(Boolean)
1316
- .map((item) => normalizeMessageRecord(item as MessageRecord))
1317
- .filter(isInsightThreadRootMessage)
1318
- .filter((message) => !domain || message.messageDomain === domain);
1319
-
1320
- const threads = await Promise.all(roots.map(async (root) => {
1321
- const messages = includeMessages
1322
- ? await loadInsightThreadMessages(root.id, root)
1323
- : [root];
1324
- return buildInsightForumThread(root, messages);
1325
- }));
1326
-
1327
- return {
1328
- threads,
1329
- nextToken: connection?.nextToken ?? null,
1330
- };
1331
- }
1332
-
1333
- export async function loadInsightForumThreadById(
1334
- threadId: string,
1335
- options: { includeMessages?: boolean } = {},
1336
- ): Promise<ForumThreadWithMessages | null> {
1337
- const normalized = String(threadId || "").trim();
1338
- if (!normalized) return null;
1339
- const testMock = getTestEditorNewsroomMock();
1340
- if (testMock?.insightForumThreads) {
1341
- return testMock.insightForumThreads.find((thread) => thread.id === normalized) ?? null;
1342
- }
1343
- const root = await getMessageRecordById(normalized);
1344
- if (!root || !isInsightThreadRootMessage(root)) return null;
1345
- const includeMessages = options.includeMessages !== false;
1346
- const messages = includeMessages
1347
- ? await loadInsightThreadMessages(normalized, root)
1348
- : [root];
1349
- return buildInsightForumThread(root, messages);
1350
- }
1351
-
1352
- export async function appendInsightThreadReplyRecord(input: {
1353
- threadId: string;
1354
- summary: string;
1355
- content: string;
1356
- role?: string;
1357
- authorLabel?: string;
1358
- parentMessageId?: string | null;
1359
- metadata?: Record<string, unknown>;
1360
- }): Promise<MessageRecord> {
1361
- const threadId = String(input.threadId || "").trim();
1362
- const summary = String(input.summary || "").trim();
1363
- const content = String(input.content || "").trim();
1364
- if (!threadId) throw new Error("threadId is required");
1365
- if (!summary) throw new Error("summary is required");
1366
- if (!content) throw new Error("content is required");
1367
- const root = await getMessageRecordById(threadId);
1368
- if (!root || !isInsightThreadRootMessage(root)) throw new Error(`Insight thread not found: ${threadId}`);
1369
- const existing = await listMessagesByThreadId(threadId, 1000);
1370
- const nextSequence = Math.max(1, ...existing.map((message) => Number(message.sequenceNumber || 0))) + 1;
1371
- const now = new Date().toISOString();
1372
- const message: MessageRecord = {
1373
- id: `message-insight-reply-${safeForumId(threadId)}-${String(nextSequence).padStart(4, "0")}`,
1374
- messageKind: "insight_reply",
1375
- messageDomain: root.messageDomain ?? "knowledge",
1376
- status: "active",
1377
- summary,
1378
- source: "newsroom.insights",
1379
- authorLabel: input.authorLabel?.trim() || "editor",
1380
- threadId,
1381
- parentMessageId: input.parentMessageId?.trim() || null,
1382
- sequenceNumber: nextSequence,
1383
- role: input.role || "human",
1384
- messageType: "insight_forum_reply",
1385
- content,
1386
- metadata: input.metadata ?? {},
1387
- createdAt: now,
1388
- updatedAt: now,
1389
- newsroomFeedKey: "messages",
1390
- };
1391
- const created = await runGraphql<{ createMessage?: MessageRecord | null }>(
1392
- CREATE_MESSAGE_MUTATION,
1393
- { input: message },
1394
- );
1395
- return normalizeMessageRecord(created.createMessage ?? message);
1396
- }
1397
-
1398
- export async function deleteInsightThreadMessageRecord(input: {
1399
- threadId: string;
1400
- messageId: string;
1401
- }): Promise<MessageRecord> {
1402
- const threadId = String(input.threadId || "").trim();
1403
- const messageId = String(input.messageId || "").trim();
1404
- if (!threadId) throw new Error("threadId is required");
1405
- if (!messageId) throw new Error("messageId is required");
1406
- const existing = await listMessagesByThreadId(threadId, 1000);
1407
- const target = existing.find((message) => message.id === messageId);
1408
- if (!target) throw new Error(`Message not found in thread: ${messageId}`);
1409
- const now = new Date().toISOString();
1410
- const updatedMessage = await runGraphql<{ updateMessage?: MessageRecord | null }>(
1411
- UPDATE_MESSAGE_MUTATION,
1412
- {
1413
- input: {
1414
- id: target.id,
1415
- status: "deleted",
1416
- updatedAt: now,
1417
- },
1418
- },
1419
- );
1420
- return normalizeMessageRecord(updatedMessage.updateMessage ?? { ...target, status: "deleted", updatedAt: now });
1421
- }
1422
-
1423
1297
  export async function ensureEditionForumThreadRecord(input: {
1424
1298
  editionId: string;
1425
1299
  title?: string;
@@ -1683,14 +1557,6 @@ async function listMessagesByThreadId(threadId: string, limit = 400): Promise<Me
1683
1557
  return records;
1684
1558
  }
1685
1559
 
1686
- function isInsightThreadRootMessage(message: MessageRecord): boolean {
1687
- if (String(message.messageKind || "") !== "insight") return false;
1688
- const threadId = String(message.threadId || "").trim();
1689
- const sequenceNumber = Number(message.sequenceNumber || 0);
1690
- if (threadId && threadId !== message.id && sequenceNumber > 1) return false;
1691
- return true;
1692
- }
1693
-
1694
1560
  async function getMessageRecordById(messageId: string): Promise<MessageRecord | null> {
1695
1561
  const normalized = String(messageId || "").trim();
1696
1562
  if (!normalized) return null;
@@ -1702,66 +1568,6 @@ async function getMessageRecordById(messageId: string): Promise<MessageRecord |
1702
1568
  return message ? normalizeMessageRecord(message) : null;
1703
1569
  }
1704
1570
 
1705
- async function loadInsightThreadMessages(threadId: string, root: MessageRecord): Promise<MessageRecord[]> {
1706
- const threadMessages = await listMessagesByThreadId(threadId, 500);
1707
- const byId = new Map<string, MessageRecord>();
1708
- byId.set(root.id, root);
1709
- for (const message of threadMessages) byId.set(message.id, message);
1710
- const merged = [...byId.values()].sort(
1711
- (left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
1712
- );
1713
- return hydrateMessageRecordsContent(merged);
1714
- }
1715
-
1716
- function insightForumThreadTitle(root: MessageRecord): string {
1717
- const metadata = normalizeJsonValue(root.metadata);
1718
- if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
1719
- const explicit = String((metadata as Record<string, unknown>).insightTitle || "").trim();
1720
- if (explicit) return explicit;
1721
- }
1722
- const summary = String(root.summary || "").trim();
1723
- return summary || "Insight";
1724
- }
1725
-
1726
- function buildInsightForumThread(root: MessageRecord, messages: MessageRecord[]): ForumThreadWithMessages {
1727
- const activeMessages = messages.filter((message) => String(message.status || "active") === "active");
1728
- const sorted = [...activeMessages].sort(
1729
- (left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
1730
- );
1731
- const lastMessage = sorted.at(-1) ?? root;
1732
- const activityAt = lastMessage.updatedAt ?? lastMessage.createdAt ?? root.updatedAt ?? root.createdAt;
1733
- return {
1734
- id: root.id,
1735
- threadKind: "insight_forum",
1736
- status: "active",
1737
- title: insightForumThreadTitle(root),
1738
- summary: formatInsightForumDomainLabel(root.messageDomain),
1739
- primaryAnchorKind: "message",
1740
- primaryAnchorId: root.id,
1741
- primaryAnchorLineageId: root.id,
1742
- primaryAnchorKey: `message#${root.id}`,
1743
- createdByLabel: root.authorLabel ?? null,
1744
- messageCount: sorted.length,
1745
- lastMessageId: lastMessage.id,
1746
- lastMessageAt: activityAt,
1747
- metadata: normalizeJsonValue(root.metadata) as Record<string, unknown>,
1748
- createdAt: root.createdAt,
1749
- updatedAt: activityAt,
1750
- newsroomFeedKey: "messages",
1751
- messages: sorted.length ? sorted : [root],
1752
- scope: "insight",
1753
- sectionKey: root.messageDomain ?? null,
1754
- };
1755
- }
1756
-
1757
- function formatInsightForumDomainLabel(domain: string | null | undefined): string {
1758
- const value = String(domain || "").trim();
1759
- if (value === "assignment_work") return "Assignment research";
1760
- if (value === "knowledge") return "Reference knowledge";
1761
- if (!value) return "Insight";
1762
- return value.replace(/_/g, " ");
1763
- }
1764
-
1765
1571
  async function hydrateMessageRecordsContent(messages: MessageRecord[]): Promise<MessageRecord[]> {
1766
1572
  return Promise.all(messages.map(async (message) => {
1767
1573
  try {
@@ -2167,7 +1973,7 @@ export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPage
2167
1973
  const CONTENT_ITEMS_BY_STAGE_QUERY = `
2168
1974
  query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
2169
1975
  listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
2170
- 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 }
2171
1977
  nextToken
2172
1978
  }
2173
1979
  }
@@ -2237,7 +2043,7 @@ type PublishedItemBoardRow = {
2237
2043
  };
2238
2044
 
2239
2045
  export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2240
- if (isLocalPreviewEnabled()) return [];
2046
+ if (isLocalPreviewEnabled()) return createLocalPreviewRealPublishedVersions();
2241
2047
  configureAmplifyClient();
2242
2048
  const client = generateClient<Schema>() as unknown as {
2243
2049
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
@@ -2691,7 +2497,6 @@ export function hasTestEditorOverride(): boolean {
2691
2497
 
2692
2498
  type TestEditorNewsroomMock = {
2693
2499
  forumThreadsByEdition?: Record<string, ForumThreadWithMessages[]>;
2694
- insightForumThreads?: ForumThreadWithMessages[];
2695
2500
  messages?: MessageRecord[];
2696
2501
  payloads?: Record<string, HydratedModelPayload[]>;
2697
2502
  referenceAttachments?: ReferenceAttachmentRecord[];
@@ -3816,6 +3621,13 @@ export async function loadCyclotronStatus(cyclotronId: string): Promise<import("
3816
3621
  return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3817
3622
  }
3818
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
+
3819
3631
  /** Ask the next decide-relevance sweep to set or clear a manual review rate. */
3820
3632
  export async function requestCyclotronReviewRate(
3821
3633
  cyclotronId: string,