@anthusai/papyrus 1.9.0 → 1.11.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 (68) hide show
  1. package/amplify/data/resource.js +3 -3
  2. package/amplify/data/resource.ts +3 -3
  3. package/amplify/functions/category-action/handler.ts +18 -18
  4. package/amplify/functions/console-chat-responder/src/main.rs +1 -1
  5. package/amplify/functions/content-actions/handler.py +23 -0
  6. package/amplify/functions/content-actions/requirements.txt +1 -1
  7. package/amplify/functions/content-actions/resource.js +1 -1
  8. package/amplify/functions/content-actions/resource.ts +1 -1
  9. package/amplify/functions/renditions/requirements.txt +1 -1
  10. package/amplify/functions/shared/slack-events.ts +2 -2
  11. package/amplify/publication/renditions-function.js +1 -1
  12. package/amplify/publication/renditions-function.ts +1 -1
  13. package/app/api/local-preview/derive/route.ts +28 -0
  14. package/app/api/local-preview/media/route.ts +25 -0
  15. package/app/globals.css +28 -28
  16. package/app/layout.tsx +8 -5
  17. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  18. package/app/newsroom/[section]/page.tsx +0 -1
  19. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  20. package/app/newsroom/insights/page.tsx +4 -12
  21. package/components/board-card-excerpts.tsx +45 -0
  22. package/components/markus-ir-preview.tsx +47 -2
  23. package/components/news-desk-page.tsx +0 -1
  24. package/components/news-desk-taxonomy-client.ts +28 -222
  25. package/components/newsroom-article-editor.tsx +138 -40
  26. package/components/newsroom-assignment-detail.tsx +79 -0
  27. package/components/newsroom-board-view.tsx +44 -9
  28. package/components/newsroom-newsprint-board.css +9 -2
  29. package/components/newsroom-ops-overview.tsx +27 -5
  30. package/components/newsroom-organizer-shell.tsx +25 -7
  31. package/components/newsroom-organizer.css +217 -6
  32. package/components/newsroom-topics-view.tsx +1 -1
  33. package/components/priming-proof-page.tsx +17 -8
  34. package/components/prospectus-account-chooser.tsx +103 -0
  35. package/components/story-reporting-trail-client.ts +91 -0
  36. package/components/story-reporting-trail.tsx +115 -0
  37. package/components/topic-steering-workspace.tsx +154 -671
  38. package/components/ui/sheet.tsx +10 -4
  39. package/components/wiki-concepts-view.tsx +153 -0
  40. package/components/wiki-index-client.ts +51 -0
  41. package/lib/approved-priming-proof-reports.ts +6 -3
  42. package/lib/approved-priming-proofs/papyrus.json +90 -0
  43. package/lib/content-actions-client.ts +3 -0
  44. package/lib/content-actions-demo.ts +55 -0
  45. package/lib/content-item-lifecycle.ts +4 -2
  46. package/lib/front-matter-fields.ts +42 -0
  47. package/lib/front-mosaic.ts +2 -1
  48. package/lib/layout-plan.ts +3 -2
  49. package/lib/layout-warnings.ts +8 -0
  50. package/lib/local-preview-seed.ts +53 -0
  51. package/lib/newsroom-articles.ts +2 -0
  52. package/lib/newsroom-assignments.ts +13 -0
  53. package/lib/newsroom-board.ts +4 -0
  54. package/lib/newsroom-forum-routes.ts +2 -43
  55. package/lib/newsroom-index-filters.ts +3 -40
  56. package/lib/newsroom-media-backend.ts +5 -0
  57. package/lib/newsroom-nav.ts +1 -3
  58. package/lib/newsroom-organizer-nav.ts +5 -12
  59. package/lib/newsroom-section-routing.ts +1 -1
  60. package/lib/newsroom-sections.ts +12 -0
  61. package/lib/papyrus-web-locations.ts +3 -24
  62. package/lib/priming-proof-report.ts +1 -0
  63. package/lib/semantic-graph.ts +5 -5
  64. package/lib/site-brand.ts +9 -0
  65. package/lib/story-reporting-trail.ts +106 -0
  66. package/lib/wiki.ts +56 -0
  67. package/package.json +14 -1
  68. package/routes.manifest.json +23 -7
@@ -1,14 +1,6 @@
1
- import { NewsDeskPage } from "../../../components/news-desk-page";
1
+ import { permanentRedirect } from "next/navigation";
2
+ import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
2
3
 
3
- export const dynamic = "force-dynamic";
4
-
5
- type NewsroomInsightsPageProps = {
6
- searchParams?: Promise<{
7
- demo?: string | string[];
8
- domain?: string | string[];
9
- }>;
10
- };
11
-
12
- export default async function NewsroomInsightsPage({ searchParams }: NewsroomInsightsPageProps) {
13
- return <NewsDeskPage section="insights" searchParams={searchParams} />;
4
+ export default function NewsroomMessagesRedirectPage() {
5
+ permanentRedirect(toPublicNewsroomPath("/newsroom/messages"));
14
6
  }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ type Excerpt = { issueId: string; text: string };
7
+
8
+ function CardExcerpt({ text }: { text: string }) {
9
+ const [expanded, setExpanded] = useState(false);
10
+ return <div className="board-card-excerpt" data-board-card-excerpt data-expanded={expanded}>
11
+ <p>{text}</p>
12
+ {text.length > 130 && <button aria-expanded={expanded} onClick={event => { event.stopPropagation(); setExpanded(open => !open); }} onKeyDown={event => event.stopPropagation()} type="button">{expanded ? "Less" : "More"}</button>}
13
+ </div>;
14
+ }
15
+
16
+ /**
17
+ * Adds a short expandable excerpt under each card's headline. The Kanbus card has no excerpt slot,
18
+ * so each excerpt is rendered into the card's content block once the card is on the page.
19
+ */
20
+ export function BoardCardExcerpts({ excerpts, scope }: { excerpts: readonly Excerpt[]; scope: HTMLElement | null }) {
21
+ const [hosts, setHosts] = useState<{ host: HTMLElement; text: string }[]>([]);
22
+ useEffect(() => {
23
+ if (!scope) return;
24
+ const collect = () => {
25
+ const found = excerpts.flatMap(({ issueId, text }) => {
26
+ const card = Array.from(scope.querySelectorAll<HTMLElement>(".issue-card")).find(candidate => candidate.dataset.issueId === issueId);
27
+ const content = card?.querySelector<HTMLElement>(".issue-card-content");
28
+ if (!content) return [];
29
+ let host = content.querySelector<HTMLElement>(":scope > [data-board-card-excerpt-host]");
30
+ if (!host) {
31
+ host = document.createElement("div");
32
+ host.dataset.boardCardExcerptHost = "true";
33
+ content.querySelector("h3")?.after(host);
34
+ }
35
+ return [{ host, text }];
36
+ });
37
+ setHosts(previous => previous.length === found.length && previous.every((entry, index) => entry.host === found[index].host && entry.text === found[index].text) ? previous : found);
38
+ };
39
+ collect();
40
+ const observer = new MutationObserver(collect);
41
+ observer.observe(scope, { childList: true, subtree: true });
42
+ return () => observer.disconnect();
43
+ }, [excerpts, scope]);
44
+ return <>{hosts.map(({ host, text }, index) => createPortal(<CardExcerpt text={text} />, host, `${index}`))}</>;
45
+ }
@@ -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 {};
@@ -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";
@@ -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,17 +1963,17 @@ 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
  }
@@ -2236,7 +2043,7 @@ type PublishedItemBoardRow = {
2236
2043
  };
2237
2044
 
2238
2045
  export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2239
- if (isLocalPreviewEnabled()) return [];
2046
+ if (isLocalPreviewEnabled()) return createLocalPreviewRealPublishedVersions();
2240
2047
  configureAmplifyClient();
2241
2048
  const client = generateClient<Schema>() as unknown as {
2242
2049
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
@@ -2690,7 +2497,6 @@ export function hasTestEditorOverride(): boolean {
2690
2497
 
2691
2498
  type TestEditorNewsroomMock = {
2692
2499
  forumThreadsByEdition?: Record<string, ForumThreadWithMessages[]>;
2693
- insightForumThreads?: ForumThreadWithMessages[];
2694
2500
  messages?: MessageRecord[];
2695
2501
  payloads?: Record<string, HydratedModelPayload[]>;
2696
2502
  referenceAttachments?: ReferenceAttachmentRecord[];