@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
@@ -18,6 +18,19 @@ export type AssignmentMetrics = {
18
18
  canceled: number;
19
19
  };
20
20
 
21
+ export const BOOKKEEPING_ASSIGNMENT_TYPE_KEYS: ReadonlySet<string> = new Set([
22
+ "analysis.reindex",
23
+ "curation.reference-intake",
24
+ "reference.corpus-accession",
25
+ "reference.identifier-backfill",
26
+ "reference.doi-backfill",
27
+ "reference.text-extraction",
28
+ ]);
29
+
30
+ export function isEditorialWorkAssignment(assignment: Pick<AssignmentRecord, "assignmentTypeKey">): boolean {
31
+ return !BOOKKEEPING_ASSIGNMENT_TYPE_KEYS.has(assignment.assignmentTypeKey?.trim() ?? "");
32
+ }
33
+
21
34
  export function assignmentTypeKeyForFilter(assignment: AssignmentRecord): string {
22
35
  return assignment.assignmentTypeKey?.trim() || "unknown";
23
36
  }
@@ -9,6 +9,7 @@ import {
9
9
  latestVersionPerContentItem,
10
10
  type ContentItemVersionRecord,
11
11
  } from "./content-item-lifecycle";
12
+ import { contentItemExcerpt } from "./front-matter-fields";
12
13
  import { assignmentStatusLabel } from "./newsroom-assignments";
13
14
  import { getContentItemName, getContentItemStageDefinitions } from "./site-brand";
14
15
 
@@ -18,6 +19,8 @@ export interface NativeBoardIssue extends KanbanIssue {
18
19
  nativeId: string;
19
20
  nativeStatus: string;
20
21
  href: string;
22
+ /** A few lines to show under the headline: the blurb, else the subtitle. */
23
+ excerpt?: string;
21
24
  }
22
25
 
23
26
  export function contentItemBoardPathSegment(): string {
@@ -57,6 +60,7 @@ export function projectContentItemBoard(records: {
57
60
  title: version.headline || version.title || version.slug, nativeStatus: stage, status: `${contentItemType}/${stage}`,
58
61
  priority: 2, description: workCount ? `${workCount} assignment${workCount === 1 ? "" : "s"} on this ${contentItemType}.` : undefined,
59
62
  updated_at: version.updatedAt ?? undefined, href: `/newsroom/articles/${encodeURIComponent(version.id)}`,
63
+ excerpt: contentItemExcerpt(version),
60
64
  };
61
65
  });
62
66
  }
@@ -1,7 +1,6 @@
1
1
  import { toPublicNewsroomPath } from "./newsroom-base-path";
2
2
 
3
3
  export const FORUM_THREAD_ROUTE_PREFIX = "/newsroom/forum/";
4
- export const INSIGHT_THREAD_ROUTE_PREFIX = "/newsroom/insights/";
5
4
  export const FORUM_MESSAGE_HASH_PREFIX = "message-";
6
5
 
7
6
  export function isForumThreadId(id: string | null | undefined): boolean {
@@ -9,11 +8,6 @@ export function isForumThreadId(id: string | null | undefined): boolean {
9
8
  return value.startsWith("message-thread-");
10
9
  }
11
10
 
12
- export function isInsightThreadId(id: string | null | undefined): boolean {
13
- const value = String(id ?? "").trim();
14
- return Boolean(value) && !isForumThreadId(value);
15
- }
16
-
17
11
  export function getForumMessageAnchorId(messageId: string): string {
18
12
  return `${FORUM_MESSAGE_HASH_PREFIX}${encodeURIComponent(messageId.trim())}`;
19
13
  }
@@ -33,7 +27,7 @@ export function parseForumMessageAnchorFromHash(hash: string | null | undefined)
33
27
  export type ParsedNewsroomForumRoute = {
34
28
  threadId: string | null;
35
29
  messageId: string | null;
36
- surface: "edition_forum" | "insight_forum" | null;
30
+ surface: "edition_forum" | null;
37
31
  };
38
32
 
39
33
  export function parseNewsroomForumRoute(pathname: string, hash?: string | null): ParsedNewsroomForumRoute {
@@ -43,13 +37,6 @@ export function parseNewsroomForumRoute(pathname: string, hash?: string | null):
43
37
  return { threadId: null, messageId: parseForumMessageAnchorFromHash(hash), surface: null };
44
38
  }
45
39
  const tail = segments.slice(newsroomIndex + 1);
46
- if (tail[0] === "insights" && tail[1]) {
47
- return {
48
- threadId: decodeURIComponent(tail[1]),
49
- messageId: parseForumMessageAnchorFromHash(hash),
50
- surface: "insight_forum",
51
- };
52
- }
53
40
  if (tail[0] === "forum" && tail[1]) {
54
41
  return {
55
42
  threadId: decodeURIComponent(tail[1]),
@@ -86,24 +73,11 @@ export function buildForumThreadUrl(
86
73
  return toPublicNewsroomPath(`/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
87
74
  }
88
75
 
89
- export function buildInsightThreadUrl(
90
- threadId: string,
91
- options?: { messageId?: string | null; demo?: boolean },
92
- ): string {
93
- const encodedThreadId = encodeURIComponent(threadId.trim());
94
- const params = new URLSearchParams();
95
- if (options?.demo) params.set("demo", "1");
96
- const query = params.toString();
97
- const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
98
- return toPublicNewsroomPath(`${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
99
- }
100
-
101
76
  export function buildThreadUrlForSurface(
102
77
  surface: ParsedNewsroomForumRoute["surface"],
103
78
  threadId: string,
104
79
  options?: { messageId?: string | null; demo?: boolean },
105
80
  ): string {
106
- if (surface === "insight_forum") return buildInsightThreadUrl(threadId, options);
107
81
  return buildForumThreadUrl(threadId, options);
108
82
  }
109
83
 
@@ -122,7 +96,7 @@ export function pushForumThreadUrl(
122
96
  const surface = options?.surface ?? readCurrentForumRoute().surface;
123
97
  const url = threadId
124
98
  ? buildThreadUrlForSurface(surface, threadId, options)
125
- : (surface === "insight_forum" ? buildNewsroomInsightsIndexUrl(options?.demo) : buildNewsroomForumIndexUrl(options?.demo));
99
+ : buildNewsroomForumIndexUrl(options?.demo);
126
100
  const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
127
101
  if (current === url) return;
128
102
  if (options?.replace) {
@@ -132,24 +106,9 @@ export function pushForumThreadUrl(
132
106
  }
133
107
  }
134
108
 
135
- export function pushInsightThreadUrl(
136
- threadId: string | null,
137
- options?: { messageId?: string | null; demo?: boolean; replace?: boolean },
138
- ) {
139
- pushForumThreadUrl(threadId, { ...options, surface: "insight_forum" });
140
- }
141
-
142
109
  export function buildNewsroomForumIndexUrl(demo?: boolean): string {
143
110
  const params = new URLSearchParams();
144
111
  if (demo) params.set("demo", "1");
145
112
  const query = params.toString();
146
113
  return toPublicNewsroomPath(query ? `/newsroom?${query}` : "/newsroom");
147
114
  }
148
-
149
- export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string): string {
150
- const params = new URLSearchParams();
151
- if (demo) params.set("demo", "1");
152
- if (domain?.trim()) params.set("domain", domain.trim());
153
- const query = params.toString();
154
- return toPublicNewsroomPath(query ? `/newsroom/insights?${query}` : "/newsroom/insights");
155
- }
@@ -1,12 +1,11 @@
1
1
  import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "./newsroom-base-path";
2
- export type NewsroomIndexTab = "references" | "messages" | "assignments" | "insights";
2
+ export type NewsroomIndexTab = "references" | "messages" | "assignments";
3
3
 
4
4
  export type ReferenceIndexOrder = "published" | "imported";
5
5
 
6
6
  export type ReferencesIndexFilters = { status: string; processing: string; order: ReferenceIndexOrder };
7
7
  export type MessagesIndexFilters = { kind: string; domain: string };
8
8
  export type AssignmentsIndexFilters = { status: string; type: string; view: string };
9
- export type InsightsIndexFilters = { domain: string };
10
9
 
11
10
  export const DEFAULT_REFERENCES_INDEX_FILTERS: ReferencesIndexFilters = {
12
11
  status: "exclude-pending",
@@ -25,10 +24,6 @@ export const DEFAULT_ASSIGNMENTS_INDEX_FILTERS: AssignmentsIndexFilters = {
25
24
  view: "queue",
26
25
  };
27
26
 
28
- export const DEFAULT_INSIGHTS_INDEX_FILTERS: InsightsIndexFilters = {
29
- domain: "",
30
- };
31
-
32
27
  export function referencesStatusFromUrl(value: string): string {
33
28
  return value === "exclude-pending" ? "__exclude_pending" : value;
34
29
  }
@@ -72,12 +67,6 @@ export function effectiveAssignmentsIndexFilters(partial?: Partial<AssignmentsIn
72
67
  };
73
68
  }
74
69
 
75
- export function effectiveInsightsIndexFilters(partial?: Partial<InsightsIndexFilters>): InsightsIndexFilters {
76
- return {
77
- domain: partial?.domain?.trim() ?? DEFAULT_INSIGHTS_INDEX_FILTERS.domain,
78
- };
79
- }
80
-
81
70
  export function readReferencesIndexFilters(searchParams: URLSearchParams): ReferencesIndexFilters {
82
71
  const statusParam = searchParams.get("status")?.trim() ?? "";
83
72
  const orderParam = searchParams.get("order")?.trim();
@@ -103,12 +92,6 @@ export function readAssignmentsIndexFilters(searchParams: URLSearchParams): Assi
103
92
  });
104
93
  }
105
94
 
106
- export function readInsightsIndexFilters(searchParams: URLSearchParams): InsightsIndexFilters {
107
- return effectiveInsightsIndexFilters({
108
- domain: searchParams.get("domain")?.trim() ?? undefined,
109
- });
110
- }
111
-
112
95
  export function buildReferencesIndexQuery(filters: ReferencesIndexFilters): string {
113
96
  const params = new URLSearchParams();
114
97
  if (filters.status && filters.status !== DEFAULT_REFERENCES_INDEX_FILTERS.status) {
@@ -137,26 +120,18 @@ export function buildAssignmentsIndexQuery(filters: AssignmentsIndexFilters): st
137
120
  return query ? `?${query}` : "";
138
121
  }
139
122
 
140
- export function buildInsightsIndexQuery(filters: InsightsIndexFilters): string {
141
- const params = new URLSearchParams();
142
- if (filters.domain.trim()) params.set("domain", filters.domain.trim());
143
- const query = params.toString();
144
- return query ? `?${query}` : "";
145
- }
146
-
147
123
  export function buildNewsroomIndexWebPath(
148
124
  tab: NewsroomIndexTab,
149
- filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters | InsightsIndexFilters,
125
+ filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters,
150
126
  ): string {
151
127
  if (tab === "references") return toPublicNewsroomPath(`/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}`);
152
128
  if (tab === "messages") return toPublicNewsroomPath(`/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}`);
153
- if (tab === "insights") return toPublicNewsroomPath(`/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}`);
154
129
  return toPublicNewsroomPath(`/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}`);
155
130
  }
156
131
 
157
132
  export function syncBrowserNewsroomIndexUrl(
158
133
  tab: NewsroomIndexTab,
159
- filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters | InsightsIndexFilters,
134
+ filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters,
160
135
  options?: { replace?: boolean },
161
136
  ) {
162
137
  if (typeof window === "undefined") return;
@@ -192,15 +167,3 @@ export function parseAssignmentIdFromNewsroomPathname(pathname: string | null |
192
167
  return segment;
193
168
  }
194
169
  }
195
-
196
- export function parseInsightThreadIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
197
- const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
198
- if (!internalPathname?.startsWith("/newsroom/insights/")) return null;
199
- const segment = internalPathname.slice("/newsroom/insights/".length).split("/")[0]?.trim() ?? "";
200
- if (!segment) return null;
201
- try {
202
- return decodeURIComponent(segment);
203
- } catch {
204
- return segment;
205
- }
206
- }
@@ -1,6 +1,8 @@
1
1
  import { generateClient } from "aws-amplify/data";
2
2
  import { getUrl, uploadData } from "aws-amplify/storage";
3
3
  import type { Schema } from "../amplify/data/resource";
4
+ import { isLocalPreviewEnabled } from "./local-preview";
5
+ import { createLocalPreviewMediaAssets } from "./local-preview-seed";
4
6
  import type { MediaAssetFields, MediaAssetRecord, NewsroomMediaBackend } from "./newsroom-media";
5
7
 
6
8
  type MediaListPage = {
@@ -113,6 +115,8 @@ export function createDemoMediaBackend(): NewsroomMediaBackend {
113
115
  rows.set(fields.itemId, [...(rows.get(fields.itemId) ?? []), fields]);
114
116
  },
115
117
  async listMediaAssets(itemId) {
118
+ const previewRows = createLocalPreviewMediaAssets(itemId);
119
+ if (previewRows.length) return previewRows;
116
120
  return (rows.get(itemId) ?? []).map((fields) => ({
117
121
  id: fields.id,
118
122
  sortKey: fields.sortKey,
@@ -122,6 +126,7 @@ export function createDemoMediaBackend(): NewsroomMediaBackend {
122
126
  }));
123
127
  },
124
128
  async imageUrl(storagePath) {
129
+ if (isLocalPreviewEnabled() && storagePath.startsWith("media/")) return `/api/local-preview/media?key=${encodeURIComponent(storagePath)}`;
125
130
  const object = objects.get(storagePath);
126
131
  if (!object) throw new Error(`No demo object at ${storagePath}.`);
127
132
  return URL.createObjectURL(object);
@@ -5,7 +5,6 @@ export type NewsroomNavTabId =
5
5
  | "overview"
6
6
  | "articles"
7
7
  | "messages"
8
- | "insights"
9
8
  | "assignments"
10
9
  | "references"
11
10
  | "topics"
@@ -28,9 +27,8 @@ export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
28
27
  { id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments"), mobilePrimary: true },
29
28
  { id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references"), mobilePrimary: true },
30
29
  { id: "messages", label: "Messages", detail: "Forum", href: newsroomHref("messages") },
31
- { id: "insights", label: "Insights", detail: "Research threads", href: newsroomHref("insights") },
32
30
  { id: "topics", label: "Topics", detail: "Taxonomy", href: newsroomHref("topics"), mobilePrimary: true },
33
- { id: "concepts", label: "Concepts", detail: "Ontology", href: newsroomHref("concepts") },
31
+ { id: "concepts", label: "Concept wiki", detail: "Knowledge graph", href: newsroomHref("concepts") },
34
32
  { id: "search", label: "Search", detail: "Knowledge base", href: newsroomHref("search") },
35
33
  { id: "administration", label: "Administration", detail: "Users & policies", href: newsroomHref("administration") },
36
34
  ];
@@ -8,29 +8,22 @@ export type OrganizerDestination = {
8
8
  };
9
9
  export const ORGANIZER_NAV_GROUPS: readonly { title: string; items: readonly OrganizerDestination[] }[] = [
10
10
  { title: "Editorial work", items: [
11
- { key: "overview", label: "Overview", description: "See the desk and choose where to begin.", path: "/newsroom", primary: true },
12
11
  { key: "articles", label: getContentItemName().plural, description: `Follow ${contentItemPluralLowercase} across their stages and open one to write or revise it.`, path: `/newsroom/${contentItemBoardPathSegment()}`, alsoActiveOnPaths: ["/newsroom/articles"], primary: true },
13
12
  { key: "assignments", label: "Assignments", description: "Follow research and reporting work across its statuses and claim it.", path: "/newsroom/assignments" },
14
13
  { key: "messages", label: "Messages", description: "Read and reply to newsroom conversations.", path: "/newsroom/messages", primary: true },
15
- { key: "insights", label: "Insights", description: "Discuss findings and research threads.", path: "/newsroom/insights" },
16
14
  ] },
17
15
  { title: "Research & knowledge", items: [
18
16
  { key: "references", label: "References", description: "Inspect sources and make editorial inclusion decisions.", path: "/newsroom/references", primary: true },
19
- { key: "desks", label: "Taxonomy", description: "Maintain topic sets and category versions.", path: "/newsroom/desks" },
20
- { key: "topics", label: "Topic proposals", description: "Review suggested changes to coverage.", path: "/newsroom/topics" },
21
- { key: "concepts", label: "Concepts", description: "Explore knowledge entities and relationships.", path: "/newsroom/concepts" },
22
- { key: "search", label: "Knowledge search", description: "Find evidence across the knowledge base.", path: "/newsroom/search" },
17
+ { key: "concepts", label: "Concepts", description: "A wiki of the entities in the knowledge graph.", path: "/newsroom/concepts" },
18
+ { key: "sections", label: "Sections", description: "Organize the publication’s sections and its ontology.", path: "/newsroom/administration/sections" },
19
+ { key: "desks", label: "Taxonomy", description: "Maintain topic sets and category versions, and review topic proposals.", path: "/newsroom/desks", alsoActiveOnPaths: ["/newsroom/topics"] },
23
20
  ] },
24
21
  { title: "Administration", items: [
25
- { key: "policies", label: "Policies", description: "Set publication and category guidance.", path: "/newsroom/administration/policies" },
26
- { key: "sections", label: "Sections", description: "Organize the publication’s sections.", path: "/newsroom/administration/sections" },
27
- { key: "procedures", label: "Procedures", description: "Manage procedure definitions and versions.", path: "/newsroom/administration/procedures", access: "admin" },
28
- { key: "users", label: "Users & identity merging", description: "Review people, roles and identity merges.", path: "/newsroom/administration/users", access: "admin" },
29
- { key: "administration", label: "Administration", description: "Open the existing CMS settings workspace.", path: "/newsroom/administration" },
22
+ { key: "administration", label: "Administration", description: "Policies, procedures, users and settings.", path: "/newsroom/administration", alsoActiveOnPaths: ["/newsroom/administration/policies", "/newsroom/administration/procedures", "/newsroom/administration/users"] },
30
23
  ] },
31
24
  ];
32
25
  const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
33
- export const ORGANIZER_PRIMARY_NAV = ["overview", "articles", "references", "messages"].map(key => all.find(item => item.key === key)!);
26
+ export const ORGANIZER_PRIMARY_NAV = ["articles", "references", "messages"].map(key => all.find(item => item.key === key)!);
34
27
 
35
28
  export const ORGANIZER_SETTINGS_DESTINATION = {
36
29
  label: "Settings", path: "/newsroom/settings", description: "Theme, motion and interface preferences.",
@@ -13,7 +13,7 @@ export function resolveNewsDeskTab(
13
13
  : raw === "desks" && !organizer ? "topics" : raw;
14
14
  switch (value) {
15
15
  case "desks": case "administration": case "topics": case "concepts":
16
- case "references": case "insights": case "messages": case "assignments": case "search":
16
+ case "references": case "messages": case "assignments": case "search":
17
17
  return value;
18
18
  default: return "overview";
19
19
  }
@@ -50,3 +50,15 @@ export function displayNewsroomSectionShortTitle(section: NewsroomSectionRecord)
50
50
  export function buildNewsroomSectionHref(sectionId: string, demo?: boolean): string {
51
51
  return getNewsroomNavHref(`/newsroom/sections/${encodeURIComponent(sectionId)}`, demo);
52
52
  }
53
+
54
+ export type NewsroomDeskKind = "core" | "rotating";
55
+ export type NewsroomDeskOption = { value: string; label: string; kind: NewsroomDeskKind };
56
+
57
+ export function newsroomDeskKind(section: NewsroomSectionRecord): NewsroomDeskKind {
58
+ return normalizeNewsroomSectionType(section.type) === "canonical" ? "core" : "rotating";
59
+ }
60
+
61
+ /** The desks a story or assignment can be placed on: the same enabled, ordered desks the Overview page lists. */
62
+ export function newsroomDeskOptions(sections: NewsroomSectionRecord[]): NewsroomDeskOption[] {
63
+ return sortNewsroomSections(sections).filter(isEnabledNewsroomSection).map(section => ({ value: section.id, label: section.title, kind: newsroomDeskKind(section) }));
64
+ }
@@ -3,12 +3,10 @@ import { PAPYRUS_OBJECT_KINDS, type PapyrusObjectKind } from "./papyrus-object-k
3
3
  import {
4
4
  buildNewsroomIndexWebPath,
5
5
  effectiveAssignmentsIndexFilters,
6
- effectiveInsightsIndexFilters,
7
6
  effectiveMessagesIndexFilters,
8
7
  effectiveReferencesIndexFilters,
9
8
  normalizeReferenceIndexOrder,
10
9
  type AssignmentsIndexFilters,
11
- type InsightsIndexFilters,
12
10
  type MessagesIndexFilters,
13
11
  type NewsroomIndexTab,
14
12
  type ReferencesIndexFilters,
@@ -27,7 +25,6 @@ export type PapyrusWebUiContext = {
27
25
  const NEWSROOM_TAB_IDS = new Set([
28
26
  "overview",
29
27
  "messages",
30
- "insights",
31
28
  "assignments",
32
29
  "references",
33
30
  "topics",
@@ -123,21 +120,10 @@ export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
123
120
  });
124
121
  }
125
122
 
126
- if ((tab === "references" || tab === "messages" || tab === "assignments" || tab === "insights") && segments.length === 2) {
123
+ if ((tab === "references" || tab === "messages" || tab === "assignments") && segments.length === 2) {
127
124
  return newsroomIndexLocation(tab as NewsroomIndexTab, normalized, url.searchParams);
128
125
  }
129
126
 
130
- if (tab === "insights" && segments[2]) {
131
- const threadId = decodeURIComponent(segments[2]);
132
- const objectUri = `papyrus://message/${encodeURIComponent(threadId)}`;
133
- return location(objectUri, normalized, {
134
- papyrusObjectUri: objectUri,
135
- newsroomTab: "insights",
136
- viewMode: "detail",
137
- label: `Insight thread ${threadId}`,
138
- });
139
- }
140
-
141
127
  if ((tab === "references" || tab === "messages" || tab === "assignments") && segments[2]) {
142
128
  const objectId = decodeURIComponent(segments[2]);
143
129
  const kind = tab === "references" ? "reference" : tab === "messages" ? "message" : "assignment";
@@ -277,7 +263,7 @@ export function papyrusUriToWebPath(uri: string): PapyrusUriToWebPathResult {
277
263
  }
278
264
 
279
265
  if (NEWSROOM_TAB_IDS.has(id)) {
280
- if (id === "references" || id === "messages" || id === "assignments" || id === "insights") {
266
+ if (id === "references" || id === "messages" || id === "assignments") {
281
267
  const mapped = newsroomIndexUriToWebPath(raw, `${id}/index`);
282
268
  if (mapped) return mapped;
283
269
  }
@@ -347,9 +333,6 @@ function readIndexFiltersFromSearchParams(tab: NewsroomIndexTab, searchParams: U
347
333
  kind: searchParams.get("kind")?.trim() ?? undefined,
348
334
  domain: searchParams.get("domain")?.trim() ?? undefined,
349
335
  });
350
- if (tab === "insights") return effectiveInsightsIndexFilters({
351
- domain: searchParams.get("domain")?.trim() ?? undefined,
352
- });
353
336
  return effectiveAssignmentsIndexFilters({
354
337
  status: searchParams.get("status")?.trim() ?? undefined,
355
338
  type: searchParams.get("type")?.trim() ?? undefined,
@@ -368,9 +351,6 @@ function buildNewsroomIndexLocationUri(tab: NewsroomIndexTab, filters: Record<st
368
351
  const normalized = filters as MessagesIndexFilters;
369
352
  if (normalized.kind?.trim()) segments.push("kind", normalized.kind.trim());
370
353
  if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
371
- } else if (tab === "insights") {
372
- const normalized = filters as InsightsIndexFilters;
373
- if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
374
354
  } else {
375
355
  const normalized = filters as AssignmentsIndexFilters;
376
356
  if (normalized.status?.trim()) segments.push("status", normalized.status.trim());
@@ -400,7 +380,7 @@ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters:
400
380
  const segments = objectId.split("/").filter(Boolean);
401
381
  if (!segments.length) return null;
402
382
  const tab = segments[0];
403
- if (tab !== "references" && tab !== "messages" && tab !== "assignments" && tab !== "insights") return null;
383
+ if (tab !== "references" && tab !== "messages" && tab !== "assignments") return null;
404
384
  if (segments.length === 1) {
405
385
  return { tab, filters: readIndexFiltersFromSearchParams(tab, new URLSearchParams()) };
406
386
  }
@@ -414,7 +394,6 @@ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters:
414
394
  }
415
395
  if (tab === "references") return { tab, filters: effectiveReferencesIndexFilters(filters) };
416
396
  if (tab === "messages") return { tab, filters: effectiveMessagesIndexFilters(filters) };
417
- if (tab === "insights") return { tab, filters: effectiveInsightsIndexFilters(filters) };
418
397
  return { tab, filters: effectiveAssignmentsIndexFilters(filters) };
419
398
  }
420
399
 
@@ -36,6 +36,7 @@ export const primingProofReportSchema = z.object({
36
36
  fingerprint: z.string().min(1),
37
37
  frozenAt: z.string().min(1),
38
38
  source: z.string().min(1),
39
+ design: z.string().min(1).optional(),
39
40
  }),
40
41
  priming: z.object({
41
42
  acceptedExamples: z.number().int().min(0),
@@ -41,7 +41,7 @@ export type SemanticPredicateId =
41
41
  | "has_editorial_form"
42
42
  | "about"
43
43
  | "comment"
44
- | "insight_about"
44
+ | "note_about"
45
45
  | "ingestion_rationale"
46
46
  | "uses_evidence"
47
47
  | "uses_signal"
@@ -77,7 +77,7 @@ export const SEMANTIC_PREDICATES: SemanticPredicateDefinition[] = [
77
77
  { id: "reference_summary_1000_tokens", label: "1000-token reference summary", group: "summarization", inverseLabel: "1000-token summary for", contextPackTags: ["reference_curation", "research", "context_ranking"] },
78
78
  { id: "mentions", label: "mentions", group: "ontology", inverseLabel: "mentioned by" },
79
79
  { id: "has_editorial_form", label: "has editorial form", group: "editorial", inverseLabel: "items by editorial form", contextPackTags: ["editing", "publication", "assignment_context"] },
80
- { id: "insight_about", label: "insight about", group: "knowledge", inverseLabel: "insights", contextPackTags: ["research", "reference_graph", "editing"] },
80
+ { id: "note_about", label: "note about", group: "knowledge", inverseLabel: "notes", contextPackTags: ["research", "reference_graph", "editing"] },
81
81
  { id: "about", label: "about", group: "commentary", inverseLabel: "commentary" },
82
82
  { id: "comment", label: "comments on", group: "commentary", inverseLabel: "commented on by", contextPackTags: ["reference_curation", "editing", "research", "assignment_context"] },
83
83
  { id: "ingestion_rationale", label: "ingestion rationale for", group: "commentary", inverseLabel: "ingestion rationale", contextPackTags: ["reference_curation", "editing", "research", "assignment_context"] },
@@ -317,13 +317,13 @@ export class SemanticGraphSnapshot {
317
317
  return ranked.map((entry) => entry.message);
318
318
  }
319
319
 
320
- insightsFor(kind: string, lineageId: string): MessageRecord[] {
320
+ notesFor(kind: string, lineageId: string): MessageRecord[] {
321
321
  return this.incoming(kind, lineageId)
322
322
  .filter((relation) => relation.subjectKind === "message")
323
- .filter((relation) => relationTypeKey(relation) === "insight_about" || relation.predicate === "insight_about")
323
+ .filter((relation) => relationTypeKey(relation) === "note_about" || relation.predicate === "note_about")
324
324
  .map((relation) => this.messagesById.get(relation.subjectId))
325
325
  .filter((message): message is MessageRecord => Boolean(message))
326
- .filter((message) => message.messageKind === "insight" && message.messageDomain === "knowledge")
326
+ .filter((message) => message.messageKind === "note" && message.messageDomain === "knowledge")
327
327
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
328
328
  }
329
329
 
package/lib/site-brand.ts CHANGED
@@ -23,6 +23,8 @@ export type SiteBrandId = string;
23
23
 
24
24
  /** Brand-specific replacements for generic Pretext components. */
25
25
  export type SiteBrandComponents = {
26
+ /** The mark shown at the top of the newsroom CMS sidebar in place of the app title. */
27
+ NewsroomLogo?: ComponentType<{ className?: string }>;
26
28
  PictogramFigure?: ComponentType<PictogramFigureProps>;
27
29
  BlogPageBackground?: ComponentType<BlogPageBackgroundProps>;
28
30
  };
@@ -79,8 +81,15 @@ export type ContentItemName = {
79
81
 
80
82
  export const DEFAULT_CONTENT_ITEM_NAME: ContentItemName = { singular: "Story", plural: "Stories" };
81
83
 
84
+ /** Browser-tab icon files under `public/`, as site-root paths. The dark variant defaults to the light one. */
85
+ export type SiteBrandIcons = { light: string; dark?: string; appleTouch?: string };
86
+
87
+ export const DEFAULT_SITE_BRAND_ICONS: Required<Pick<SiteBrandIcons, "light" | "dark">> & { fallback: string } = { light: "/icon-light.png", dark: "/icon-dark.png", fallback: "/icon.png" };
88
+
82
89
  export type SiteBrand = {
83
90
  id: SiteBrandId;
91
+ /** The browser-tab icon. Omit to use the Papyrus plant. */
92
+ icons?: SiteBrandIcons;
84
93
  appTitle: string;
85
94
  appDescription: string;
86
95
  mastheadTitle: string;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Everything the reporting process recorded about one story, gathered by the story's lineage id:
3
+ * the draft versions of the copy, the research, reporting and copywriting assignments with their events,
4
+ * the notes and packets in the threads anchored to the story or its assignments, the references it draws on,
5
+ * and attached files. See docs/content-item-lifecycle.md for how these records join.
6
+ */
7
+ export type TrailVersion = {
8
+ id: string;
9
+ versionNumber: number;
10
+ versionState: string | null;
11
+ status: string | null;
12
+ stage: string | null;
13
+ headline: string | null;
14
+ changeReason: string | null;
15
+ versionCreatedBy: string | null;
16
+ versionCreatedAt: string | null;
17
+ updatedAt: string | null;
18
+ };
19
+
20
+ export type TrailAssignmentEvent = {
21
+ id: string;
22
+ eventType: string;
23
+ fromStatus: string | null;
24
+ toStatus: string | null;
25
+ actorLabel: string | null;
26
+ note: string | null;
27
+ createdAt: string;
28
+ };
29
+
30
+ export type TrailAssignment = {
31
+ id: string;
32
+ title: string;
33
+ assignmentTypeKey: string;
34
+ status: string;
35
+ assigneeKey: string | null;
36
+ summary: string | null;
37
+ createdAt: string;
38
+ events: TrailAssignmentEvent[];
39
+ };
40
+
41
+ export type TrailMessage = {
42
+ id: string;
43
+ sequenceNumber?: number | null;
44
+ messageKind: string | null;
45
+ authorLabel: string | null;
46
+ summary: string | null;
47
+ content: string | null;
48
+ createdAt: string;
49
+ };
50
+
51
+ export type TrailThread = {
52
+ id: string;
53
+ title: string | null;
54
+ threadKind: string | null;
55
+ summary: string | null;
56
+ messages: TrailMessage[];
57
+ };
58
+
59
+ export type TrailReference = {
60
+ relationId: string;
61
+ predicate: string;
62
+ referenceId: string;
63
+ referenceLineageId: string | null;
64
+ title: string | null;
65
+ sourceUri: string | null;
66
+ /** consulted: read during reporting; cited: a citation in the article. */
67
+ role: "consulted" | "cited";
68
+ note: string | null;
69
+ citationKey: string | null;
70
+ addedBy: string | null;
71
+ addedAt: string | null;
72
+ };
73
+
74
+ export type TrailAttachment = {
75
+ id: string;
76
+ role: string | null;
77
+ filename: string | null;
78
+ mediaType: string | null;
79
+ byteSize: number | null;
80
+ };
81
+
82
+ export type StoryReportingTrail = {
83
+ versions: TrailVersion[];
84
+ assignments: TrailAssignment[];
85
+ threads: TrailThread[];
86
+ references: TrailReference[];
87
+ attachments: TrailAttachment[];
88
+ };
89
+
90
+ export const EMPTY_STORY_REPORTING_TRAIL: StoryReportingTrail = { versions: [], assignments: [], threads: [], references: [], attachments: [] };
91
+
92
+ export function storyReportingTrailIsEmpty(trail: StoryReportingTrail): boolean {
93
+ return trail.assignments.length === 0 && trail.threads.length === 0 && trail.references.length === 0 && trail.attachments.length === 0 && trail.versions.length <= 1;
94
+ }
95
+
96
+ export type StoryFeedEntry =
97
+ | ({ entryKind: "message"; threadTitle: string | null } & TrailMessage)
98
+ | { entryKind: "reference"; id: string; createdAt: string; reference: TrailReference };
99
+
100
+ /** Every message of the story's threads and every reference found for it in one chronological feed, newest first. */
101
+ export function storyMessageFeed(trail: StoryReportingTrail): StoryFeedEntry[] {
102
+ const messages: StoryFeedEntry[] = trail.threads.flatMap(thread => thread.messages.map(message => ({ ...message, entryKind: "message" as const, threadTitle: thread.title })));
103
+ const references: StoryFeedEntry[] = trail.references.filter(reference => reference.addedAt).map(reference => ({ entryKind: "reference" as const, id: `reference-${reference.relationId}`, createdAt: reference.addedAt as string, reference }));
104
+ const sequenceOf = (entry: StoryFeedEntry) => (entry.entryKind === "message" ? entry.sequenceNumber ?? 0 : 0);
105
+ return [...messages, ...references].sort((left, right) => right.createdAt.localeCompare(left.createdAt) || sequenceOf(right) - sequenceOf(left));
106
+ }