@anthusai/papyrus 0.0.0 → 1.0.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 (282) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2069 -0
  5. package/amplify/data/resource.ts +2185 -0
  6. package/amplify/data/schema.js +1882 -0
  7. package/amplify/data/schema.ts +1990 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/content-actions/handler.py +149 -0
  22. package/amplify/functions/content-actions/requirements.txt +4 -0
  23. package/amplify/functions/content-actions/resource.js +40 -0
  24. package/amplify/functions/content-actions/resource.ts +54 -0
  25. package/amplify/functions/email-submission-processor/handler.py +46 -0
  26. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  27. package/amplify/functions/email-submission-processor/resource.js +56 -0
  28. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  29. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  30. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  31. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  32. package/amplify/functions/knowledge-query/handler.py +1 -0
  33. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  34. package/amplify/functions/knowledge-query/resource.js +50 -0
  35. package/amplify/functions/knowledge-query/resource.ts +61 -0
  36. package/amplify/functions/manage-user-role/handler.ts +799 -0
  37. package/amplify/functions/manage-user-role/resource.js +8 -0
  38. package/amplify/functions/manage-user-role/resource.ts +9 -0
  39. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  40. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  41. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  42. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  43. package/amplify/functions/newsroom-summary/resource.js +8 -0
  44. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  45. package/amplify/functions/procedure-action/handler.ts +621 -0
  46. package/amplify/functions/procedure-action/resource.js +8 -0
  47. package/amplify/functions/procedure-action/resource.ts +9 -0
  48. package/amplify/functions/reader-settings/handler.ts +322 -0
  49. package/amplify/functions/reader-settings/resource.js +8 -0
  50. package/amplify/functions/reader-settings/resource.ts +9 -0
  51. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  52. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  53. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  54. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  55. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  56. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  57. package/amplify/functions/shared/email-submission.ts +415 -0
  58. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  59. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  60. package/amplify/functions/shared/model-payloads.ts +186 -0
  61. package/amplify/functions/shared/python-bundle.js +130 -0
  62. package/amplify/functions/shared/python-bundle.ts +162 -0
  63. package/amplify/functions/shared/slack-events.ts +111 -0
  64. package/amplify/functions/slack-delivery/handler.py +112 -0
  65. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  66. package/amplify/functions/slack-delivery/resource.js +38 -0
  67. package/amplify/functions/slack-delivery/resource.ts +51 -0
  68. package/amplify/functions/slack-events/handler.ts +155 -0
  69. package/amplify/functions/slack-events/resource.js +11 -0
  70. package/amplify/functions/slack-events/resource.ts +12 -0
  71. package/amplify/inbound-email/stack.js +46 -0
  72. package/amplify/inbound-email/stack.ts +61 -0
  73. package/amplify/package.json +3 -0
  74. package/amplify/rebuild-trigger-settings.js +22 -0
  75. package/amplify/rebuild-trigger-settings.ts +42 -0
  76. package/amplify/site-backend-config.js +1 -0
  77. package/amplify/site-backend-config.ts +36 -0
  78. package/amplify/site-backend.js +615 -0
  79. package/amplify/site-backend.ts +739 -0
  80. package/amplify/storage/resource.js +21 -0
  81. package/amplify/storage/resource.ts +22 -0
  82. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  83. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  84. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  85. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  86. package/app/[year]/[month]/[day]/page.tsx +22 -0
  87. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  88. package/app/api/archive/editions/route.ts +18 -0
  89. package/app/api/media/[...path]/route.ts +35 -0
  90. package/app/api/revalidate/route.ts +94 -0
  91. package/app/archive/page.tsx +24 -0
  92. package/app/articles/[slug]/page.tsx +35 -0
  93. package/app/globals.css +10345 -0
  94. package/app/layout.tsx +142 -0
  95. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  96. package/app/newsroom/[section]/page.tsx +47 -0
  97. package/app/newsroom/articles/[id]/page.tsx +15 -0
  98. package/app/newsroom/articles/page.tsx +13 -0
  99. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  100. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  101. package/app/newsroom/insights/page.tsx +14 -0
  102. package/app/newsroom/layout.tsx +12 -0
  103. package/app/newsroom/page.tsx +31 -0
  104. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  105. package/app/page.tsx +272 -0
  106. package/app/papyrus-theme.css +11 -0
  107. package/app/robots.ts +11 -0
  108. package/app/settings/page.tsx +14 -0
  109. package/app/tailwind.css +65 -0
  110. package/bin/papyrus-app.mjs +20 -0
  111. package/bin/papyrus-infra.mjs +95 -0
  112. package/components/ai-elements/conversation.tsx +168 -0
  113. package/components/ai-elements/message.tsx +360 -0
  114. package/components/ai-elements/model-selector.tsx +30 -0
  115. package/components/ai-elements/prompt-input.tsx +1461 -0
  116. package/components/ai-elements/shimmer.tsx +76 -0
  117. package/components/ai-elements/suggestion.tsx +54 -0
  118. package/components/ai-elements/tool.tsx +173 -0
  119. package/components/amplify-client-provider.tsx +140 -0
  120. package/components/archive-grid.tsx +89 -0
  121. package/components/archive-shell.tsx +41 -0
  122. package/components/article-video.tsx +156 -0
  123. package/components/blog-page-background.tsx +20 -0
  124. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  125. package/components/markus-ir-preview.tsx +300 -0
  126. package/components/news-desk-client-provider.tsx +211 -0
  127. package/components/news-desk-page.tsx +116 -0
  128. package/components/news-desk-taxonomy-client.ts +3684 -0
  129. package/components/newspaper.tsx +1929 -0
  130. package/components/newsroom-access-gate.tsx +66 -0
  131. package/components/newsroom-article-editor.tsx +741 -0
  132. package/components/newsroom-articles-view.tsx +129 -0
  133. package/components/newsroom-assignments-view.tsx +664 -0
  134. package/components/newsroom-ops-overview.tsx +226 -0
  135. package/components/newsroom-ops-shell.tsx +321 -0
  136. package/components/newsroom-references-view.tsx +598 -0
  137. package/components/newsroom-topics-view.tsx +425 -0
  138. package/components/papyrus-console-shell.tsx +1912 -0
  139. package/components/pictogram-figure.tsx +65 -0
  140. package/components/presentation-footer.tsx +146 -0
  141. package/components/reader-auth-control.tsx +162 -0
  142. package/components/reader-auth-state.ts +122 -0
  143. package/components/reader-settings.ts +332 -0
  144. package/components/reader-theme-control.tsx +85 -0
  145. package/components/reference-source-preview.tsx +163 -0
  146. package/components/settings-page.tsx +239 -0
  147. package/components/staging-banner.tsx +11 -0
  148. package/components/topic-steering-workspace.tsx +18005 -0
  149. package/components/ui/badge.tsx +30 -0
  150. package/components/ui/button-group.tsx +87 -0
  151. package/components/ui/button.tsx +58 -0
  152. package/components/ui/card.tsx +32 -0
  153. package/components/ui/command.tsx +194 -0
  154. package/components/ui/dialog.tsx +158 -0
  155. package/components/ui/dropdown-menu.tsx +266 -0
  156. package/components/ui/hover-card.tsx +51 -0
  157. package/components/ui/input-group.tsx +158 -0
  158. package/components/ui/input.tsx +20 -0
  159. package/components/ui/scroll-area.tsx +16 -0
  160. package/components/ui/select.tsx +199 -0
  161. package/components/ui/separator.tsx +23 -0
  162. package/components/ui/sheet.tsx +92 -0
  163. package/components/ui/spinner.tsx +10 -0
  164. package/components/ui/tabs.tsx +113 -0
  165. package/components/ui/textarea.tsx +18 -0
  166. package/components/ui/tooltip.tsx +66 -0
  167. package/components/use-resolved-papyrus-theme.ts +47 -0
  168. package/infra/amplify-app-shell.js +234 -0
  169. package/infra/amplify-app-shell.ts +284 -0
  170. package/infra/build-specs.js +85 -0
  171. package/infra/build-specs.ts +131 -0
  172. package/infra/github-oidc-provider.js +24 -0
  173. package/infra/github-oidc-provider.ts +30 -0
  174. package/infra/index.js +15 -0
  175. package/infra/site-config.js +186 -0
  176. package/infra/site-config.ts +235 -0
  177. package/lib/amplify-outputs-guard.ts +24 -0
  178. package/lib/amplify-outputs-path.ts +16 -0
  179. package/lib/amplify-server-runtime.ts +63 -0
  180. package/lib/archive-data.ts +30 -0
  181. package/lib/archive-types.ts +15 -0
  182. package/lib/articles.ts +320 -0
  183. package/lib/cached-content-repository.ts +90 -0
  184. package/lib/category-dashboard.ts +37 -0
  185. package/lib/category-repository.ts +1907 -0
  186. package/lib/console-chat-client.ts +1316 -0
  187. package/lib/console-web-context.ts +118 -0
  188. package/lib/content-actions-client.ts +98 -0
  189. package/lib/content-actions-demo.ts +190 -0
  190. package/lib/content-load-timing.ts +23 -0
  191. package/lib/content-repository.ts +43 -0
  192. package/lib/content-source-context.ts +26 -0
  193. package/lib/content-types.ts +104 -0
  194. package/lib/define-site.js +3 -0
  195. package/lib/define-site.ts +21 -0
  196. package/lib/demo-amplify-outputs.ts +15 -0
  197. package/lib/dev-sandbox-editor-auth.ts +18 -0
  198. package/lib/doctrine.ts +128 -0
  199. package/lib/edition-routes.ts +155 -0
  200. package/lib/edition-sections.ts +111 -0
  201. package/lib/empty-edition-layout-plan.ts +109 -0
  202. package/lib/empty-theme.css +1 -0
  203. package/lib/excerpts.ts +8 -0
  204. package/lib/graphql-content-repository.ts +1140 -0
  205. package/lib/image-url.ts +10 -0
  206. package/lib/layout-plan.ts +981 -0
  207. package/lib/layout-scenarios.ts +564 -0
  208. package/lib/markus-body.ts +168 -0
  209. package/lib/markus-ir.ts +235 -0
  210. package/lib/markus-projection.ts +243 -0
  211. package/lib/markus-to-article.ts +93 -0
  212. package/lib/news-desk-session.ts +160 -0
  213. package/lib/newspaper-layout.ts +3459 -0
  214. package/lib/newsroom-articles.ts +325 -0
  215. package/lib/newsroom-assignments.ts +173 -0
  216. package/lib/newsroom-base-path.ts +48 -0
  217. package/lib/newsroom-card-layout.ts +102 -0
  218. package/lib/newsroom-category-drilldown.ts +133 -0
  219. package/lib/newsroom-demo-articles.ts +90 -0
  220. package/lib/newsroom-demo-dashboard.ts +267 -0
  221. package/lib/newsroom-demo-profile.ts +46 -0
  222. package/lib/newsroom-forum-routes.ts +153 -0
  223. package/lib/newsroom-index-filters.ts +202 -0
  224. package/lib/newsroom-knowledge-query-request.ts +51 -0
  225. package/lib/newsroom-list-selection.ts +42 -0
  226. package/lib/newsroom-media-backend.ts +142 -0
  227. package/lib/newsroom-media.ts +182 -0
  228. package/lib/newsroom-nav.ts +44 -0
  229. package/lib/newsroom-references.ts +116 -0
  230. package/lib/newsroom-sections.ts +52 -0
  231. package/lib/newsroom-topics.ts +118 -0
  232. package/lib/papyrus-object-kinds.ts +24 -0
  233. package/lib/papyrus-web-locations.ts +480 -0
  234. package/lib/pilobol-sample.ts +167 -0
  235. package/lib/presentation-footer.ts +92 -0
  236. package/lib/pretext-layout.ts +179 -0
  237. package/lib/public-placeholder-config.ts +102 -0
  238. package/lib/publication-items.ts +181 -0
  239. package/lib/reader-route-config.ts +1 -0
  240. package/lib/reader-static-params.ts +56 -0
  241. package/lib/reader-storage-url.ts +63 -0
  242. package/lib/reference-display.ts +74 -0
  243. package/lib/reference-policy.ts +97 -0
  244. package/lib/reference-source-preview.ts +175 -0
  245. package/lib/renderer-config.ts +50 -0
  246. package/lib/renderer.ts +87 -0
  247. package/lib/reporting-story-budget.ts +638 -0
  248. package/lib/semantic-graph.ts +554 -0
  249. package/lib/site-brand-tenant.ts +22 -0
  250. package/lib/site-brand.ts +194 -0
  251. package/lib/site-env.ts +52 -0
  252. package/lib/site-renderer.ts +17 -0
  253. package/lib/site-stack.ts +195 -0
  254. package/lib/staging-gate.ts +37 -0
  255. package/lib/staging-preview-object.ts +98 -0
  256. package/lib/themed-image.ts +30 -0
  257. package/lib/utils.ts +6 -0
  258. package/lib/video-mode.ts +42 -0
  259. package/lib/video-script.ts +63 -0
  260. package/middleware.ts +103 -0
  261. package/package.json +318 -1
  262. package/renderers/markus/index.ts +25 -0
  263. package/renderers/markus/stubs.tsx +33 -0
  264. package/renderers/pretext/article-page.tsx +169 -0
  265. package/renderers/pretext/index.ts +30 -0
  266. package/renderers/pretext/layouts.ts +16 -0
  267. package/renderers/pretext/presentation-shell.tsx +470 -0
  268. package/routes.manifest.json +208 -0
  269. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  270. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  271. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  272. package/scripts/videoml/bundle-common.mjs +129 -0
  273. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  274. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  275. package/scripts/videoml/preview/preview-timing.ts +42 -0
  276. package/scripts/videoml/preview.html +77 -0
  277. package/scripts/videoml/standard-entry.tsx +46 -0
  278. package/src/backend.js +1 -0
  279. package/src/backend.ts +3 -0
  280. package/src/sync.mjs +87 -0
  281. package/src/with-papyrus.d.mts +2 -0
  282. package/src/with-papyrus.mjs +60 -0
@@ -0,0 +1,133 @@
1
+ import type {
2
+ CategorySteeringCategory,
3
+ SemanticRelationRecord,
4
+ } from "./category-repository";
5
+ import type {
6
+ SemanticNeighborGroup,
7
+ SemanticObjectSummary,
8
+ } from "./semantic-graph";
9
+
10
+ export type CategoryDrilldownContext = {
11
+ aliases: string[];
12
+ categories: CategorySteeringCategory[];
13
+ includeDescendants: boolean;
14
+ primary: CategorySteeringCategory | null;
15
+ };
16
+
17
+ type CategoryGraph = {
18
+ neighbors(kind: string, lineageId: string): SemanticNeighborGroup[];
19
+ referencesForCategory(categoryLineageId: string): SemanticObjectSummary[];
20
+ };
21
+
22
+ export function categoryLineageId(category: CategorySteeringCategory): string {
23
+ return category.lineageId ?? category.id;
24
+ }
25
+
26
+ export function buildCategoryDrilldownContext(categories: CategorySteeringCategory[], selection?: string | null): CategoryDrilldownContext {
27
+ if (!selection) return { aliases: [], categories: [], includeDescendants: false, primary: null };
28
+ const directMatches = uniqueCategoryRecords(categories.filter((category) => matchesCategorySelection(category, selection)));
29
+ const primary = directMatches[0] ?? null;
30
+ const selectedCategoryKey = primary?.categoryKey ?? selection;
31
+ const matchingCategoryKeyRecords = uniqueCategoryRecords(categories.filter((category) => category.categoryKey === selectedCategoryKey));
32
+ const directRecords = uniqueCategoryRecords([...directMatches, ...matchingCategoryKeyRecords]);
33
+ const includeDescendants = directRecords.some((category) => !category.parentCategoryKey);
34
+ const childRecords = includeDescendants
35
+ ? categories.filter((category) => category.parentCategoryKey === selectedCategoryKey)
36
+ : [];
37
+ const records = uniqueCategoryRecords([...directRecords, ...childRecords]);
38
+ return {
39
+ aliases: categoryAliasValues(records),
40
+ categories: records,
41
+ includeDescendants,
42
+ primary: primary ?? records[0] ?? null,
43
+ };
44
+ }
45
+
46
+ export function buildTopicDrilldownContext(
47
+ root: { category: CategorySteeringCategory; subcategorys: CategorySteeringCategory[] },
48
+ selected: CategorySteeringCategory,
49
+ categoryByUid: Map<string, CategorySteeringCategory>,
50
+ ): CategoryDrilldownContext {
51
+ const selectedFlatCategory = categoryByUid.get(selected.categoryKey) ?? null;
52
+ const directRecords = uniqueCategoryRecords([selected, selectedFlatCategory].filter((category): category is CategorySteeringCategory => Boolean(category)));
53
+ const includeDescendants = selected.categoryKey === root.category.categoryKey;
54
+ const childRecords = includeDescendants ? root.subcategorys : [];
55
+ const records = uniqueCategoryRecords([...directRecords, ...childRecords]);
56
+ return {
57
+ aliases: categoryAliasValues(records),
58
+ categories: records,
59
+ includeDescendants,
60
+ primary: directRecords[0] ?? records[0] ?? null,
61
+ };
62
+ }
63
+
64
+ export function referencesForCategoryContext(graph: CategoryGraph, context: CategoryDrilldownContext): SemanticObjectSummary[] {
65
+ return uniqueSemanticSummaries(context.aliases.flatMap((alias) => graph.referencesForCategory(alias)));
66
+ }
67
+
68
+ export function semanticNodesForCategoryContext(graph: CategoryGraph, context: CategoryDrilldownContext): SemanticObjectSummary[] {
69
+ return uniqueSemanticSummaries(
70
+ context.aliases.flatMap((alias) => graph.neighbors("category", alias))
71
+ .flatMap((group) => group.objects)
72
+ .filter((object) => object.kind === "semanticNode"),
73
+ );
74
+ }
75
+
76
+ export function uniqueNeighborGroupsForCategoryContext(graph: CategoryGraph, context: CategoryDrilldownContext): SemanticNeighborGroup[] {
77
+ const groups = new Map<string, SemanticNeighborGroup>();
78
+ for (const alias of context.aliases) {
79
+ for (const group of graph.neighbors("category", alias)) {
80
+ const key = `${group.direction}#${group.predicate}`;
81
+ const current = groups.get(key);
82
+ if (!current) {
83
+ groups.set(key, { ...group, relations: [...group.relations], objects: [...group.objects] });
84
+ } else {
85
+ current.relations = uniqueRelations([...current.relations, ...group.relations]);
86
+ current.objects = uniqueSemanticSummaries([...current.objects, ...group.objects]);
87
+ }
88
+ }
89
+ }
90
+ return Array.from(groups.values());
91
+ }
92
+
93
+ export function topicHref(rootCategoryKey: string, selectedCategoryKey?: string | null): string {
94
+ const encodedRoot = encodeURIComponent(rootCategoryKey);
95
+ const encodedSelected = selectedCategoryKey && selectedCategoryKey !== rootCategoryKey ? `/${encodeURIComponent(selectedCategoryKey)}` : "";
96
+ return `/newsroom/topics/${encodedRoot}${encodedSelected}`;
97
+ }
98
+
99
+ export function categoryDrilldownHref(section: "references" | "concepts", categoryKey: string): string {
100
+ return `/newsroom/${section}/${encodeURIComponent(categoryKey)}`;
101
+ }
102
+
103
+ function matchesCategorySelection(category: CategorySteeringCategory, selection: string): boolean {
104
+ return category.id === selection || category.lineageId === selection || category.categoryKey === selection;
105
+ }
106
+
107
+ function uniqueCategoryRecords(categories: CategorySteeringCategory[]): CategorySteeringCategory[] {
108
+ const records = new Map<string, CategorySteeringCategory>();
109
+ for (const category of categories) {
110
+ records.set(category.id, category);
111
+ }
112
+ return Array.from(records.values());
113
+ }
114
+
115
+ function categoryAliasValues(categories: CategorySteeringCategory[]): string[] {
116
+ const aliases = new Set<string>();
117
+ for (const category of categories) {
118
+ aliases.add(category.id);
119
+ aliases.add(category.categoryKey);
120
+ if (category.lineageId) aliases.add(category.lineageId);
121
+ }
122
+ return Array.from(aliases).filter(Boolean);
123
+ }
124
+
125
+ function uniqueRelations(relations: SemanticRelationRecord[]): SemanticRelationRecord[] {
126
+ return Array.from(new Map(relations.map((relation) => [relation.id, relation])).values());
127
+ }
128
+
129
+ function uniqueSemanticSummaries(objects: SemanticObjectSummary[]): SemanticObjectSummary[] {
130
+ const map = new Map<string, SemanticObjectSummary>();
131
+ for (const object of objects) map.set(`${object.kind}#${object.lineageId}`, object);
132
+ return Array.from(map.values()).sort((left, right) => left.label.localeCompare(right.label));
133
+ }
@@ -0,0 +1,90 @@
1
+ export type DemoArticleRecord = {
2
+ id: string;
3
+ type: string;
4
+ slug: string;
5
+ section: string | null;
6
+ title: string;
7
+ status: "draft" | "published";
8
+ frontMatterYaml: string;
9
+ bodyMarkus: string;
10
+ contentHash: string;
11
+ publishedContentHash: string | null;
12
+ versionNumber: number | null;
13
+ everPublished: boolean;
14
+ updatedAt: string;
15
+ };
16
+
17
+ export function demoContentHash(frontMatterYaml: string, bodyMarkus: string): string {
18
+ let hash = 5381;
19
+ const text = `${frontMatterYaml}\u0000${bodyMarkus}`;
20
+ for (let index = 0; index < text.length; index += 1) {
21
+ hash = ((hash << 5) + hash + text.charCodeAt(index)) >>> 0;
22
+ }
23
+ return `demo:${hash.toString(16)}`;
24
+ }
25
+
26
+ function demoRecord(
27
+ fields: Pick<DemoArticleRecord, "id" | "slug" | "section" | "title" | "bodyMarkus" | "updatedAt"> & {
28
+ type?: string;
29
+ status: "draft" | "published";
30
+ publishedBody?: string;
31
+ versionNumber: number | null;
32
+ },
33
+ ): DemoArticleRecord {
34
+ const frontMatterYaml = `title: ${fields.title}\n`;
35
+ const contentHash = demoContentHash(frontMatterYaml, fields.bodyMarkus);
36
+ const publishedContentHash = fields.status === "published"
37
+ ? demoContentHash(frontMatterYaml, fields.publishedBody ?? fields.bodyMarkus)
38
+ : null;
39
+ return {
40
+ id: fields.id,
41
+ type: fields.type ?? "article",
42
+ slug: fields.slug,
43
+ section: fields.section,
44
+ title: fields.title,
45
+ status: fields.status,
46
+ frontMatterYaml,
47
+ bodyMarkus: fields.bodyMarkus,
48
+ contentHash,
49
+ publishedContentHash,
50
+ versionNumber: fields.versionNumber,
51
+ everPublished: fields.status === "published",
52
+ updatedAt: fields.updatedAt,
53
+ };
54
+ }
55
+
56
+ export function createDemoArticleRecords(): DemoArticleRecord[] {
57
+ return [
58
+ demoRecord({
59
+ id: "demo-article-changed",
60
+ slug: "forty-billion-dollars",
61
+ section: "business",
62
+ title: "Forty Billion Dollars",
63
+ bodyMarkus: "The raise doubled again this quarter.\n\nAnalysts expect the pace to slow.\n",
64
+ publishedBody: "The raise doubled this quarter.\n",
65
+ status: "published",
66
+ versionNumber: 2,
67
+ updatedAt: "2026-10-05T16:00:00.000Z",
68
+ }),
69
+ demoRecord({
70
+ id: "demo-article-published",
71
+ slug: "harbor-cranes-return",
72
+ section: "local",
73
+ title: "Harbor Cranes Return",
74
+ bodyMarkus: "# Harbor Cranes Return\n\nThe cranes were lifted back into place on Monday.\n",
75
+ status: "published",
76
+ versionNumber: 1,
77
+ updatedAt: "2026-10-04T12:00:00.000Z",
78
+ }),
79
+ demoRecord({
80
+ id: "demo-article-draft",
81
+ slug: "council-budget-notes",
82
+ section: "politics",
83
+ title: "Council Budget Notes",
84
+ bodyMarkus: "Notes from the budget session.\n",
85
+ status: "draft",
86
+ versionNumber: null,
87
+ updatedAt: "2026-10-03T09:00:00.000Z",
88
+ }),
89
+ ];
90
+ }
@@ -0,0 +1,267 @@
1
+ import type { CategorySteeringDashboard } from "./category-repository";
2
+ import { getNewsroomDemoProfile, type NewsroomDemoProfile } from "./newsroom-demo-profile";
3
+ import type { SiteBrandId } from "./site-brand";
4
+
5
+ const PILOBOLUS_CATEGORY_LABELS: Record<string, { displayName: string; subtitle: string; description: string }> = {
6
+ "category.foundation-model-scaling": {
7
+ displayName: "Strangler-Fig Control Loops",
8
+ subtitle: "Host encirclement, canopy pressure, and takeover timing",
9
+ description: "Field notes on how strangler-fig encirclement reshapes access paths and control timing in Pilobolus research.",
10
+ },
11
+ "category.agent-memory": {
12
+ displayName: "Mycelial Signal Routing",
13
+ subtitle: "Network timing, propagation failure, and recovery paths",
14
+ description: "Accepted subcategory for signal routing, persistence, and coordination across fungal network observations.",
15
+ },
16
+ "category.benchmark-saturation": {
17
+ displayName: "Canopy Competition Metrics",
18
+ subtitle: "Light capture, host stress, and encirclement saturation signals",
19
+ description: "Metrics for when a host canopy loses effective control under sustained strangler pressure.",
20
+ },
21
+ "category.symbolic-connectionist-history": {
22
+ displayName: "Fungal Network Field Logs",
23
+ subtitle: "Seasonal intake logs and annotated field observations",
24
+ description: "Historical field logs and intake notes that anchor Pilobolus fungus-among-us coverage.",
25
+ },
26
+ };
27
+
28
+ export function applyPilobolusDemoDashboardContent(
29
+ dashboard: CategorySteeringDashboard,
30
+ profile: NewsroomDemoProfile = getNewsroomDemoProfile("pilobol-us"),
31
+ ): CategorySteeringDashboard {
32
+ const localized = structuredClone(dashboard);
33
+ localized.canonicalCorpusId = profile.corpusId;
34
+ localized.canonicalCategorySetId = profile.categorySetId;
35
+
36
+ for (const corpus of localized.corpora) {
37
+ if (corpus.role === "canonical") {
38
+ corpus.id = profile.corpusId;
39
+ corpus.name = profile.canonicalCorpusName;
40
+ }
41
+ if (corpus.role === "source") {
42
+ corpus.id = profile.sourceCorpusId;
43
+ corpus.name = profile.sourceCorpusName;
44
+ }
45
+ }
46
+
47
+ for (const run of localized.importRuns) {
48
+ run.classifierId = profile.classifierId;
49
+ if (run.corpusId.includes("demo-canonical")) run.corpusId = profile.corpusId;
50
+ if (run.corpusId.includes("demo-source")) run.corpusId = profile.sourceCorpusId;
51
+ }
52
+
53
+ for (const set of localized.categorySets) {
54
+ set.classifierId = profile.classifierId;
55
+ if (set.id === "category-set-demo-canonical") {
56
+ set.id = profile.categorySetId;
57
+ set.lineageId = profile.categorySetId;
58
+ set.corpusId = profile.corpusId;
59
+ set.displayName = profile.categorySetLabel;
60
+ set.description = "Accepted Pilobolus steering tree for fungus-among-us field coverage.";
61
+ }
62
+ if (set.id === "category-set-demo-source") {
63
+ set.id = profile.sourceCategorySetId;
64
+ set.lineageId = profile.sourceCategorySetId;
65
+ set.corpusId = profile.sourceCorpusId;
66
+ set.displayName = "Pilobolus Source Intake Tree";
67
+ set.description = "Source intake categories for Pilobolus field material.";
68
+ }
69
+ }
70
+
71
+ for (const tree of localized.categoryTrees) {
72
+ tree.classifierId = profile.classifierId;
73
+ tree.corpusId = profile.corpusId;
74
+ tree.displayName = `${profile.brandLabel} Steering Tree`;
75
+ tree.description = "Accepted topic tree for Pilobolus field-notes coverage.";
76
+ }
77
+
78
+ const relabelCategory = <T extends { categoryKey?: string | null; displayName?: string | null; subtitle?: string | null; description?: string | null }>(
79
+ row: T,
80
+ ): T => {
81
+ const labels = row.categoryKey ? PILOBOLUS_CATEGORY_LABELS[row.categoryKey] : undefined;
82
+ if (!labels) return row;
83
+ return {
84
+ ...row,
85
+ displayName: labels.displayName,
86
+ subtitle: labels.subtitle,
87
+ description: labels.description,
88
+ };
89
+ };
90
+
91
+ localized.categorys = localized.categorys.map(relabelCategory);
92
+ localized.categoryNodes = localized.categoryNodes.map(relabelCategory);
93
+
94
+ localized.categoryKeywords = localized.categoryKeywords.map((keyword) => {
95
+ if (keyword.categoryKey === "category.foundation-model-scaling") {
96
+ return { ...keyword, keyword: "strangler fig", normalizedKeyword: "strangler fig" };
97
+ }
98
+ if (keyword.categoryKey === "category.agent-memory") {
99
+ return { ...keyword, keyword: "mycelial routing", normalizedKeyword: "mycelial routing" };
100
+ }
101
+ return keyword;
102
+ });
103
+
104
+ localized.proposals = localized.proposals.map((proposal) => {
105
+ const relabeled = relabelCategory(proposal);
106
+ if (proposal.proposalKind === "rename-category") {
107
+ return {
108
+ ...relabeled,
109
+ title: "Rename strangler-fig control category",
110
+ summary: "Field intake consistently uses strangler-fig encirclement language instead of generic scaling terminology.",
111
+ };
112
+ }
113
+ if (proposal.proposalKind === "create-category") {
114
+ return {
115
+ ...relabeled,
116
+ title: "Create mycelial routing subcategory",
117
+ summary: "Discovery found a child topic under strangler-fig control loops for signal routing evidence.",
118
+ displayName: "Mycelial Signal Routing",
119
+ description: "Candidate subcategory covering routing, persistence, and propagation across fungal networks.",
120
+ };
121
+ }
122
+ if (proposal.proposalKind === "relationship-proposal") {
123
+ return {
124
+ ...relabeled,
125
+ title: "Link control loop category to canopy metrics entity",
126
+ summary: "Evidence suggests strangler-fig control loops relate to canopy competition metrics.",
127
+ };
128
+ }
129
+ return relabeled;
130
+ });
131
+
132
+ localized.references = localized.references.map((reference, index) => ({
133
+ ...reference,
134
+ corpusId: profile.sourceCorpusId,
135
+ title: index === 0
136
+ ? "Strangler Fig Host Encirclement Field Notes"
137
+ : "Mycelial Coordination Under Canopy Stress",
138
+ sourceUri: index === 0
139
+ ? "s3://pilobol-us-demo/corpora/pilobol-us-source/strangler-fig-field-notes-001.md"
140
+ : "s3://pilobol-us-demo/corpora/pilobol-us-source/mycelial-canopy-stress-002.md",
141
+ storagePath: index === 0
142
+ ? "corpora/pilobol-us-source/strangler-fig-field-notes-001.md"
143
+ : "corpora/pilobol-us-source/mycelial-canopy-stress-002.md",
144
+ }));
145
+
146
+ localized.referenceAttachments = localized.referenceAttachments.map((attachment, index) => ({
147
+ ...attachment,
148
+ sourceUri: index % 2 === 0
149
+ ? "s3://pilobol-us-demo/corpora/pilobol-us-source/strangler-fig-field-notes-001.md"
150
+ : "s3://pilobol-us-demo/corpora/pilobol-us-source/mycelial-canopy-stress-002.md",
151
+ storagePath: index % 2 === 0
152
+ ? "corpora/pilobol-us-source/strangler-fig-field-notes-001.md"
153
+ : "corpora/pilobol-us-source/mycelial-canopy-stress-002.md",
154
+ filename: index % 2 === 0 ? "strangler-fig-field-notes-001.md" : "mycelial-canopy-stress-002.md",
155
+ }));
156
+
157
+ localized.semanticNodes = localized.semanticNodes.map((node, index) => ({
158
+ ...node,
159
+ corpusId: index === 0 ? profile.corpusId : profile.sourceCorpusId,
160
+ displayName: index === 0 ? "Canopy Competition Metrics" : "Intake Rationale",
161
+ }));
162
+
163
+ localized.messages = localized.messages.map((message) => {
164
+ if (message.messageKind === "ingestion_rationale") {
165
+ return {
166
+ ...message,
167
+ body: "Imported as a useful holdout against generic platform demo intake.",
168
+ summary: "Pilobolus intake rationale",
169
+ };
170
+ }
171
+ if (message.messageKind === "reporting_context_packet") {
172
+ const metadata = message.metadata && typeof message.metadata === "object" && !Array.isArray(message.metadata)
173
+ ? message.metadata as Record<string, unknown>
174
+ : {};
175
+ const reporting = metadata.reporting && typeof metadata.reporting === "object" && !Array.isArray(metadata.reporting)
176
+ ? metadata.reporting as Record<string, unknown>
177
+ : {};
178
+ return {
179
+ ...message,
180
+ body: "Context packet for a Pilobolus field-notes candidate. Editors can select, merge, brief, hold, or kill this packet without creating reader copy.",
181
+ summary: "Reporting context packet: strangler-fig control-loop angle",
182
+ metadata: {
183
+ ...metadata,
184
+ reporting: {
185
+ ...reporting,
186
+ sectionKey: "field-notes",
187
+ recommendedAngle: "Focus on host encirclement timing and mycelial routing evidence.",
188
+ copywriterBrief: "Use accepted Pilobolus field-note evidence before drafting.",
189
+ },
190
+ },
191
+ };
192
+ }
193
+ return message;
194
+ });
195
+
196
+ localized.assignments = localized.assignments.map((assignment, index) => {
197
+ if (assignment.assignmentTypeKey === "reporting.edition-candidate") {
198
+ return {
199
+ ...assignment,
200
+ title: "Field-notes reporting candidate 1: Strangler-fig control-loop shift",
201
+ summary: "Create reporting context for a Pilobolus field-notes candidate; do not create a draft Item until editor selection.",
202
+ brief: "Report a section-ready candidate for Field Notes, option 1.",
203
+ primaryFocusCategoryKey: "category.foundation-model-scaling",
204
+ topicScopeCategoryKeys: ["category.foundation-model-scaling"],
205
+ };
206
+ }
207
+ if (index === 0) {
208
+ return {
209
+ ...assignment,
210
+ title: "Reference intake: strangler-fig field notes",
211
+ summary: "Review and curate Pilobolus source intake for strangler-fig encirclement field notes.",
212
+ };
213
+ }
214
+ return {
215
+ ...assignment,
216
+ title: "Reference intake: mycelial canopy stress log",
217
+ summary: "Review Pilobolus source intake for mycelial coordination under canopy stress.",
218
+ };
219
+ });
220
+
221
+ localized.doctrineRecords = localized.doctrineRecords.map((record) => {
222
+ const editorial = record.editorial && typeof record.editorial === "object" && !Array.isArray(record.editorial)
223
+ ? record.editorial as Record<string, unknown>
224
+ : null;
225
+ if (editorial?.kind === "mission") {
226
+ return {
227
+ ...record,
228
+ title: "Pilobolus Editorial Mission",
229
+ body: [
230
+ "Pilobolus publishes field notes that help readers see fungus-among-us coordination, strangler-fig control loops, and mycelial signal routing with inspectable evidence.",
231
+ "Coverage should stay source-disciplined, canopy-aware, and explicit about what remains uncertain in the field record.",
232
+ ],
233
+ };
234
+ }
235
+ if (editorial?.kind === "policy") {
236
+ return {
237
+ ...record,
238
+ title: "Pilobolus Editorial Policy",
239
+ body: [
240
+ "Publication doctrine is the global inclusion standard for Pilobol.us; desk doctrine sets local intake priorities for field notes and source intake.",
241
+ "Field-notes reporting should prioritize accepted Pilobolus references and annotated intake artifacts—not the Papyrus platform knowledge base.",
242
+ "High-risk claims about host takeover or network control require explicit uncertainty handling and accepted source linkage.",
243
+ ],
244
+ };
245
+ }
246
+ return record;
247
+ });
248
+
249
+ localized.artifacts = localized.artifacts.map((artifact) => ({
250
+ ...artifact,
251
+ corpusId: profile.corpusId,
252
+ artifactId: "s3://pilobol-us-demo/accepted-category-set.json",
253
+ displayName: "Pilobolus Accepted Steering Tree JSON",
254
+ }));
255
+
256
+ localized.lexicalSteeringRules = localized.lexicalSteeringRules.map((rule) => ({
257
+ ...rule,
258
+ source: "pilobol-us-lexical-steering.yml",
259
+ createdBy: "pilobol-us-config",
260
+ }));
261
+
262
+ return localized;
263
+ }
264
+
265
+ export function shouldUsePilobolusDemoDashboard(brandId: SiteBrandId): boolean {
266
+ return brandId === "pilobol-us";
267
+ }
@@ -0,0 +1,46 @@
1
+ import { resolveSiteBrandId, type SiteBrandId } from "./site-brand";
2
+
3
+ export type NewsroomDemoProfile = {
4
+ brandLabel: string;
5
+ canonicalCorpusName: string;
6
+ sourceCorpusName: string;
7
+ categorySetLabel: string;
8
+ classifierId: string;
9
+ corpusId: string;
10
+ sourceCorpusId: string;
11
+ categorySetId: string;
12
+ sourceCategorySetId: string;
13
+ };
14
+
15
+ const PAPYRUS_DEMO_PROFILE: NewsroomDemoProfile = {
16
+ brandLabel: "Papyrus",
17
+ canonicalCorpusName: "Canonical Demo Corpus",
18
+ sourceCorpusName: "Source Demo Corpus",
19
+ categorySetLabel: "Demo Canonical Category Set",
20
+ classifierId: "demo-canonical-classifier",
21
+ corpusId: "knowledge-corpus-demo-canonical",
22
+ sourceCorpusId: "knowledge-corpus-demo-source",
23
+ categorySetId: "category-set-demo-canonical",
24
+ sourceCategorySetId: "category-set-demo-source",
25
+ };
26
+
27
+ const PILOBOLUS_DEMO_PROFILE: NewsroomDemoProfile = {
28
+ brandLabel: "Pilobolus",
29
+ canonicalCorpusName: "Pilobolus Field Notes Corpus",
30
+ sourceCorpusName: "Pilobolus Source Intake",
31
+ categorySetLabel: "Pilobolus Steering Tree",
32
+ classifierId: "pilobolus-demo-classifier",
33
+ corpusId: "knowledge-corpus-pilobolus-demo-canonical",
34
+ sourceCorpusId: "knowledge-corpus-pilobolus-demo-source",
35
+ categorySetId: "category-set-pilobolus-demo-canonical",
36
+ sourceCategorySetId: "category-set-pilobolus-demo-source",
37
+ };
38
+
39
+ export function getNewsroomDemoProfile(brandId: SiteBrandId = resolveSiteBrandId()): NewsroomDemoProfile {
40
+ if (brandId === "pilobol-us") return PILOBOLUS_DEMO_PROFILE;
41
+ return PAPYRUS_DEMO_PROFILE;
42
+ }
43
+
44
+ export function isPilobolusDemoBrand(brandId: SiteBrandId = resolveSiteBrandId()): boolean {
45
+ return brandId === "pilobol-us";
46
+ }
@@ -0,0 +1,153 @@
1
+ export const FORUM_THREAD_ROUTE_PREFIX = "/newsroom/forum/";
2
+ export const INSIGHT_THREAD_ROUTE_PREFIX = "/newsroom/insights/";
3
+ export const FORUM_MESSAGE_HASH_PREFIX = "message-";
4
+
5
+ export function isForumThreadId(id: string | null | undefined): boolean {
6
+ const value = String(id ?? "").trim();
7
+ return value.startsWith("message-thread-");
8
+ }
9
+
10
+ export function isInsightThreadId(id: string | null | undefined): boolean {
11
+ const value = String(id ?? "").trim();
12
+ return Boolean(value) && !isForumThreadId(value);
13
+ }
14
+
15
+ export function getForumMessageAnchorId(messageId: string): string {
16
+ return `${FORUM_MESSAGE_HASH_PREFIX}${encodeURIComponent(messageId.trim())}`;
17
+ }
18
+
19
+ export function parseForumMessageAnchorFromHash(hash: string | null | undefined): string | null {
20
+ const raw = String(hash ?? "").replace(/^#/, "").trim();
21
+ if (!raw.startsWith(FORUM_MESSAGE_HASH_PREFIX)) return null;
22
+ const encoded = raw.slice(FORUM_MESSAGE_HASH_PREFIX.length);
23
+ if (!encoded) return null;
24
+ try {
25
+ return decodeURIComponent(encoded);
26
+ } catch {
27
+ return encoded;
28
+ }
29
+ }
30
+
31
+ export type ParsedNewsroomForumRoute = {
32
+ threadId: string | null;
33
+ messageId: string | null;
34
+ surface: "edition_forum" | "insight_forum" | null;
35
+ };
36
+
37
+ export function parseNewsroomForumRoute(pathname: string, hash?: string | null): ParsedNewsroomForumRoute {
38
+ const segments = pathname.split("/").filter(Boolean);
39
+ const newsroomIndex = segments.indexOf("newsroom");
40
+ if (newsroomIndex < 0) {
41
+ return { threadId: null, messageId: parseForumMessageAnchorFromHash(hash), surface: null };
42
+ }
43
+ const tail = segments.slice(newsroomIndex + 1);
44
+ if (tail[0] === "insights" && tail[1]) {
45
+ return {
46
+ threadId: decodeURIComponent(tail[1]),
47
+ messageId: parseForumMessageAnchorFromHash(hash),
48
+ surface: "insight_forum",
49
+ };
50
+ }
51
+ if (tail[0] === "forum" && tail[1]) {
52
+ return {
53
+ threadId: decodeURIComponent(tail[1]),
54
+ messageId: parseForumMessageAnchorFromHash(hash),
55
+ surface: "edition_forum",
56
+ };
57
+ }
58
+ if (tail[0] === "messages" && tail[1] === "forum" && tail[2]) {
59
+ return {
60
+ threadId: decodeURIComponent(tail[2]),
61
+ messageId: parseForumMessageAnchorFromHash(hash),
62
+ surface: "edition_forum",
63
+ };
64
+ }
65
+ if (tail[0] === "messages" && tail[1] && isForumThreadId(tail[1])) {
66
+ return {
67
+ threadId: decodeURIComponent(tail[1]),
68
+ messageId: parseForumMessageAnchorFromHash(hash),
69
+ surface: "edition_forum",
70
+ };
71
+ }
72
+ return { threadId: null, messageId: parseForumMessageAnchorFromHash(hash), surface: null };
73
+ }
74
+
75
+ export function buildForumThreadUrl(
76
+ threadId: string,
77
+ options?: { messageId?: string | null; demo?: boolean },
78
+ ): string {
79
+ const encodedThreadId = encodeURIComponent(threadId.trim());
80
+ const params = new URLSearchParams();
81
+ if (options?.demo) params.set("demo", "1");
82
+ const query = params.toString();
83
+ const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
84
+ return `/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
85
+ }
86
+
87
+ export function buildInsightThreadUrl(
88
+ threadId: string,
89
+ options?: { messageId?: string | null; demo?: boolean },
90
+ ): string {
91
+ const encodedThreadId = encodeURIComponent(threadId.trim());
92
+ const params = new URLSearchParams();
93
+ if (options?.demo) params.set("demo", "1");
94
+ const query = params.toString();
95
+ const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
96
+ return `${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
97
+ }
98
+
99
+ export function buildThreadUrlForSurface(
100
+ surface: ParsedNewsroomForumRoute["surface"],
101
+ threadId: string,
102
+ options?: { messageId?: string | null; demo?: boolean },
103
+ ): string {
104
+ if (surface === "insight_forum") return buildInsightThreadUrl(threadId, options);
105
+ return buildForumThreadUrl(threadId, options);
106
+ }
107
+
108
+ export function readCurrentForumRoute(): ParsedNewsroomForumRoute {
109
+ if (typeof window === "undefined") {
110
+ return { threadId: null, messageId: null, surface: null };
111
+ }
112
+ return parseNewsroomForumRoute(window.location.pathname, window.location.hash);
113
+ }
114
+
115
+ export function pushForumThreadUrl(
116
+ threadId: string | null,
117
+ options?: { messageId?: string | null; demo?: boolean; replace?: boolean; surface?: ParsedNewsroomForumRoute["surface"] },
118
+ ) {
119
+ if (typeof window === "undefined") return;
120
+ const surface = options?.surface ?? readCurrentForumRoute().surface;
121
+ const url = threadId
122
+ ? buildThreadUrlForSurface(surface, threadId, options)
123
+ : (surface === "insight_forum" ? buildNewsroomInsightsIndexUrl(options?.demo) : buildNewsroomForumIndexUrl(options?.demo));
124
+ const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
125
+ if (current === url) return;
126
+ if (options?.replace) {
127
+ window.history.replaceState(null, "", url);
128
+ } else {
129
+ window.history.pushState(null, "", url);
130
+ }
131
+ }
132
+
133
+ export function pushInsightThreadUrl(
134
+ threadId: string | null,
135
+ options?: { messageId?: string | null; demo?: boolean; replace?: boolean },
136
+ ) {
137
+ pushForumThreadUrl(threadId, { ...options, surface: "insight_forum" });
138
+ }
139
+
140
+ export function buildNewsroomForumIndexUrl(demo?: boolean): string {
141
+ const params = new URLSearchParams();
142
+ if (demo) params.set("demo", "1");
143
+ const query = params.toString();
144
+ return query ? `/newsroom?${query}` : "/newsroom";
145
+ }
146
+
147
+ export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string): string {
148
+ const params = new URLSearchParams();
149
+ if (demo) params.set("demo", "1");
150
+ if (domain?.trim()) params.set("domain", domain.trim());
151
+ const query = params.toString();
152
+ return query ? `/newsroom/insights?${query}` : "/newsroom/insights";
153
+ }