@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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 (254) 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 +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -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/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,480 @@
1
+ import { PAPYRUS_OBJECT_KINDS, type PapyrusObjectKind } from "./papyrus-object-kinds";
2
+ import {
3
+ buildNewsroomIndexWebPath,
4
+ effectiveAssignmentsIndexFilters,
5
+ effectiveInsightsIndexFilters,
6
+ effectiveMessagesIndexFilters,
7
+ effectiveReferencesIndexFilters,
8
+ normalizeReferenceIndexOrder,
9
+ type AssignmentsIndexFilters,
10
+ type InsightsIndexFilters,
11
+ type MessagesIndexFilters,
12
+ type NewsroomIndexTab,
13
+ type ReferencesIndexFilters,
14
+ } from "./newsroom-index-filters";
15
+
16
+ export type PapyrusWebUiContext = {
17
+ webPath: string;
18
+ papyrusLocationUri: string;
19
+ papyrusObjectUri?: string | null;
20
+ newsroomTab?: string | null;
21
+ viewMode?: "index" | "detail" | null;
22
+ indexFilters?: Record<string, string> | null;
23
+ label?: string | null;
24
+ };
25
+
26
+ const NEWSROOM_TAB_IDS = new Set([
27
+ "overview",
28
+ "messages",
29
+ "insights",
30
+ "assignments",
31
+ "references",
32
+ "topics",
33
+ "concepts",
34
+ "administration",
35
+ "search",
36
+ "sections",
37
+ ]);
38
+
39
+ export function buildWebUiContext(webPath: string): PapyrusWebUiContext {
40
+ const location = webPathToPapyrusLocation(webPath);
41
+ return {
42
+ webPath: location.webPath,
43
+ papyrusLocationUri: location.papyrusLocationUri,
44
+ papyrusObjectUri: location.papyrusObjectUri ?? null,
45
+ newsroomTab: location.newsroomTab ?? null,
46
+ viewMode: location.viewMode ?? null,
47
+ indexFilters: location.indexFilters ?? null,
48
+ label: location.label ?? null,
49
+ };
50
+ }
51
+
52
+ export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
53
+ const normalized = normalizeWebPath(webPath);
54
+ const url = new URL(normalized, "https://papyrus.local");
55
+ const pathname = url.pathname || "/";
56
+
57
+ if (pathname === "/" || pathname === "") {
58
+ return location("papyrus://site/home", normalized, { label: "Papyrus home" });
59
+ }
60
+ if (pathname === "/archive") {
61
+ return location("papyrus://site/archive", normalized, { label: "Archive" });
62
+ }
63
+ if (pathname === "/settings") {
64
+ return location("papyrus://site/settings", normalized, { label: "Settings" });
65
+ }
66
+ if (pathname.startsWith("/articles/")) {
67
+ const slug = decodeURIComponent(pathname.slice("/articles/".length).replace(/\/+$/, ""));
68
+ if (slug) {
69
+ const objectUri = `papyrus://item/${encodeURIComponent(slug)}`;
70
+ return location(objectUri, normalized, { papyrusObjectUri: objectUri, label: `Article ${slug}` });
71
+ }
72
+ }
73
+
74
+ const editionArticle = matchEditionArticlePath(pathname);
75
+ if (editionArticle) {
76
+ const objectUri = `papyrus://item/${encodeURIComponent(editionArticle.slug)}`;
77
+ return location(objectUri, normalized, {
78
+ papyrusObjectUri: objectUri,
79
+ label: `Edition article ${editionArticle.year}-${editionArticle.month}-${editionArticle.day} / ${editionArticle.slug}`,
80
+ });
81
+ }
82
+
83
+ if (!pathname.startsWith("/newsroom")) {
84
+ const tail = pathname.replace(/^\/+/, "");
85
+ return location(`papyrus://site/path/${encodeURIComponent(tail)}`, normalized, { label: pathname });
86
+ }
87
+
88
+ if (pathname === "/newsroom" || pathname === "/newsroom/") {
89
+ return location("papyrus://newsroom/overview", normalized, { newsroomTab: "overview", label: "Newsroom overview" });
90
+ }
91
+
92
+ const segments = pathname.split("/").filter(Boolean);
93
+ const tab = segments[1] ?? "overview";
94
+
95
+ if (tab === "sections" && segments[2]) {
96
+ const sectionId = decodeURIComponent(segments[2]);
97
+ const objectUri = `papyrus://newsroomSection/${encodeURIComponent(sectionId)}`;
98
+ return location(objectUri, normalized, {
99
+ papyrusObjectUri: objectUri,
100
+ newsroomTab: "sections",
101
+ label: `Newsroom section ${sectionId}`,
102
+ });
103
+ }
104
+
105
+ if (tab === "search") {
106
+ const anchorUri = searchAnchorUri(url.searchParams);
107
+ const locationUri = anchorUri
108
+ ? `papyrus://newsroom/search/${anchorUri.replace(/^papyrus:\/\//, "")}`
109
+ : "papyrus://newsroom/search";
110
+ return location(locationUri, normalized, {
111
+ papyrusObjectUri: anchorUri,
112
+ newsroomTab: "search",
113
+ label: "Newsroom search",
114
+ });
115
+ }
116
+
117
+ if (tab === "administration") {
118
+ const panel = segments.slice(2).join("/") || "overview";
119
+ return location(`papyrus://newsroom/administration/${encodeURIComponent(panel)}`, normalized, {
120
+ newsroomTab: "administration",
121
+ label: `Newsroom administration / ${panel}`,
122
+ });
123
+ }
124
+
125
+ if ((tab === "references" || tab === "messages" || tab === "assignments" || tab === "insights") && segments.length === 2) {
126
+ return newsroomIndexLocation(tab as NewsroomIndexTab, normalized, url.searchParams);
127
+ }
128
+
129
+ if (tab === "insights" && segments[2]) {
130
+ const threadId = decodeURIComponent(segments[2]);
131
+ const objectUri = `papyrus://message/${encodeURIComponent(threadId)}`;
132
+ return location(objectUri, normalized, {
133
+ papyrusObjectUri: objectUri,
134
+ newsroomTab: "insights",
135
+ viewMode: "detail",
136
+ label: `Insight thread ${threadId}`,
137
+ });
138
+ }
139
+
140
+ if ((tab === "references" || tab === "messages" || tab === "assignments") && segments[2]) {
141
+ const objectId = decodeURIComponent(segments[2]);
142
+ const kind = tab === "references" ? "reference" : tab === "messages" ? "message" : "assignment";
143
+ const objectUri = `papyrus://${kind}/${encodeURIComponent(objectId)}`;
144
+ return location(objectUri, normalized, {
145
+ papyrusObjectUri: objectUri,
146
+ newsroomTab: tab,
147
+ viewMode: "detail",
148
+ label: `${kind} ${objectId}`,
149
+ });
150
+ }
151
+
152
+ if (tab === "topics") {
153
+ const category = url.searchParams.get("category")?.trim();
154
+ if (category) {
155
+ const objectUri = `papyrus://category/${encodeURIComponent(category)}`;
156
+ return location(objectUri, normalized, {
157
+ papyrusObjectUri: objectUri,
158
+ newsroomTab: "topics",
159
+ label: `Category ${category}`,
160
+ });
161
+ }
162
+ return location("papyrus://newsroom/topics", normalized, { newsroomTab: "topics", label: "Newsroom topics" });
163
+ }
164
+
165
+ if (tab === "concepts") {
166
+ const node = url.searchParams.get("node")?.trim();
167
+ if (node) {
168
+ const objectUri = `papyrus://semanticNode/${encodeURIComponent(node)}`;
169
+ return location(objectUri, normalized, {
170
+ papyrusObjectUri: objectUri,
171
+ newsroomTab: "concepts",
172
+ label: `Semantic node ${node}`,
173
+ });
174
+ }
175
+ const category = url.searchParams.get("category")?.trim();
176
+ if (category) {
177
+ const objectUri = `papyrus://category/${encodeURIComponent(category)}`;
178
+ return location(objectUri, normalized, {
179
+ papyrusObjectUri: objectUri,
180
+ newsroomTab: "concepts",
181
+ label: `Category ${category}`,
182
+ });
183
+ }
184
+ return location("papyrus://newsroom/concepts", normalized, { newsroomTab: "concepts", label: "Newsroom concepts" });
185
+ }
186
+
187
+ if (NEWSROOM_TAB_IDS.has(tab)) {
188
+ return location(`papyrus://newsroom/${encodeURIComponent(tab)}`, normalized, {
189
+ newsroomTab: tab,
190
+ label: `Newsroom ${tab}`,
191
+ });
192
+ }
193
+
194
+ const remainder = segments.slice(1).join("/");
195
+ return location(`papyrus://newsroom/${encodeURIComponent(remainder)}`, normalized, {
196
+ newsroomTab: tab,
197
+ label: `Newsroom ${remainder}`,
198
+ });
199
+ }
200
+
201
+ export type PapyrusUriToWebPathResult = {
202
+ ok: true;
203
+ webPath: string;
204
+ papyrusLocationUri: string;
205
+ papyrusObjectUri?: string;
206
+ viewMode?: "index" | "detail";
207
+ indexFilters?: Record<string, string>;
208
+ };
209
+
210
+ export function papyrusUriToWebPath(uri: string): PapyrusUriToWebPathResult {
211
+ const raw = uri.trim();
212
+ if (!raw) throw new Error("Papyrus location URI is required");
213
+
214
+ const parsed = parsePapyrusUri(raw);
215
+ const { kind, id } = parsed;
216
+
217
+ if (kind === "site") {
218
+ if (id === "home") return { ok: true, papyrusLocationUri: raw, webPath: "/" };
219
+ if (id === "archive") return { ok: true, papyrusLocationUri: raw, webPath: "/archive" };
220
+ if (id === "settings") return { ok: true, papyrusLocationUri: raw, webPath: "/settings" };
221
+ if (id.startsWith("path/")) {
222
+ return { ok: true, papyrusLocationUri: raw, webPath: `/${decodeURIComponent(id.slice("path/".length))}` };
223
+ }
224
+ throw new Error(`Unsupported site location URI: ${raw}`);
225
+ }
226
+
227
+ if (PAPYRUS_OBJECT_KINDS.has(kind as PapyrusObjectKind)) {
228
+ const webPath = objectKindToWebPath(kind as PapyrusObjectKind, id);
229
+ return { ok: true, papyrusLocationUri: raw, papyrusObjectUri: raw, webPath };
230
+ }
231
+
232
+ if (kind !== "newsroom") {
233
+ throw new Error(`Unsupported Papyrus location URI kind: ${kind}`);
234
+ }
235
+
236
+ if (id === "overview") return { ok: true, papyrusLocationUri: raw, webPath: "/newsroom" };
237
+ if (id === "assignments/budget") {
238
+ return {
239
+ ok: true,
240
+ papyrusLocationUri: raw,
241
+ webPath: "/newsroom/assignments?view=budget",
242
+ viewMode: "index",
243
+ indexFilters: { view: "budget" },
244
+ };
245
+ }
246
+
247
+ const indexMapped = newsroomIndexUriToWebPath(raw, id);
248
+ if (indexMapped) return indexMapped;
249
+
250
+ if (id === "search") return { ok: true, papyrusLocationUri: raw, webPath: "/newsroom/search" };
251
+
252
+ if (id.startsWith("search/")) {
253
+ const anchorTail = id.slice("search/".length);
254
+ const slash = anchorTail.indexOf("/");
255
+ if (slash < 0) throw new Error(`Invalid anchored search location URI: ${raw}`);
256
+ const anchorKind = canonicalObjectKind(anchorTail.slice(0, slash));
257
+ const anchorId = decodeURIComponent(anchorTail.slice(slash + 1));
258
+ if (!anchorKind) throw new Error(`Invalid anchored search location URI: ${raw}`);
259
+ const params = new URLSearchParams({
260
+ anchorKind,
261
+ anchorId,
262
+ anchorLineageId: anchorId,
263
+ });
264
+ const objectUri = `papyrus://${anchorKind}/${encodeURIComponent(anchorId)}`;
265
+ return {
266
+ ok: true,
267
+ papyrusLocationUri: raw,
268
+ papyrusObjectUri: objectUri,
269
+ webPath: `/newsroom/search?${params.toString()}`,
270
+ };
271
+ }
272
+
273
+ if (id.startsWith("administration/")) {
274
+ const panel = decodeURIComponent(id.slice("administration/".length));
275
+ return { ok: true, papyrusLocationUri: raw, webPath: `/newsroom/administration/${panel}` };
276
+ }
277
+
278
+ if (NEWSROOM_TAB_IDS.has(id)) {
279
+ if (id === "references" || id === "messages" || id === "assignments" || id === "insights") {
280
+ const mapped = newsroomIndexUriToWebPath(raw, `${id}/index`);
281
+ if (mapped) return mapped;
282
+ }
283
+ return { ok: true, papyrusLocationUri: raw, webPath: id === "overview" ? "/newsroom" : `/newsroom/${id}` };
284
+ }
285
+
286
+ if (id.includes("/")) {
287
+ return { ok: true, papyrusLocationUri: raw, webPath: `/newsroom/${decodeURIComponent(id)}` };
288
+ }
289
+
290
+ throw new Error(`Unsupported newsroom location URI: ${raw}`);
291
+ }
292
+
293
+ export function extractNavigationIntent(toolResultJson: unknown): { webPath: string; papyrusLocationUri: string } | null {
294
+ if (!toolResultJson || typeof toolResultJson !== "object" || Array.isArray(toolResultJson)) return null;
295
+ const envelope = toolResultJson as Record<string, unknown>;
296
+ if (envelope.ok !== true) return null;
297
+ const value = envelope.value;
298
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
299
+ const navigation = (value as Record<string, unknown>).navigation;
300
+ if (!navigation || typeof navigation !== "object" || Array.isArray(navigation)) return null;
301
+ const webPath = (navigation as Record<string, unknown>).webPath;
302
+ const papyrusLocationUri = (navigation as Record<string, unknown>).papyrusLocationUri;
303
+ if (typeof webPath !== "string" || !webPath.trim()) return null;
304
+ if (typeof papyrusLocationUri !== "string" || !papyrusLocationUri.trim()) return null;
305
+ return { webPath: webPath.trim(), papyrusLocationUri: papyrusLocationUri.trim() };
306
+ }
307
+
308
+ function location(
309
+ papyrusLocationUri: string,
310
+ webPath: string,
311
+ extras?: Partial<PapyrusWebUiContext>,
312
+ ): PapyrusWebUiContext {
313
+ return {
314
+ webPath,
315
+ papyrusLocationUri,
316
+ papyrusObjectUri: extras?.papyrusObjectUri ?? null,
317
+ newsroomTab: extras?.newsroomTab ?? null,
318
+ viewMode: extras?.viewMode ?? null,
319
+ indexFilters: extras?.indexFilters ?? null,
320
+ label: extras?.label ?? null,
321
+ };
322
+ }
323
+
324
+ function newsroomIndexLocation(tab: NewsroomIndexTab, webPath: string, searchParams: URLSearchParams): PapyrusWebUiContext {
325
+ const filters = readIndexFiltersFromSearchParams(tab, searchParams);
326
+ const locationUri = buildNewsroomIndexLocationUri(tab, filters);
327
+ const label = [`Newsroom ${tab} index`, ...Object.entries(filters).filter(([, value]) => value).map(([key, value]) => `${key}=${value}`)].join(" ");
328
+ return location(locationUri, webPath, {
329
+ newsroomTab: tab,
330
+ viewMode: "index",
331
+ indexFilters: filters,
332
+ label,
333
+ });
334
+ }
335
+
336
+ function readIndexFiltersFromSearchParams(tab: NewsroomIndexTab, searchParams: URLSearchParams): Record<string, string> {
337
+ if (tab === "references") {
338
+ const orderParam = searchParams.get("order")?.trim();
339
+ return effectiveReferencesIndexFilters({
340
+ status: searchParams.get("status")?.trim() || undefined,
341
+ processing: searchParams.get("processing")?.trim() ?? undefined,
342
+ order: orderParam ? normalizeReferenceIndexOrder(orderParam) : undefined,
343
+ });
344
+ }
345
+ if (tab === "messages") return effectiveMessagesIndexFilters({
346
+ kind: searchParams.get("kind")?.trim() ?? undefined,
347
+ domain: searchParams.get("domain")?.trim() ?? undefined,
348
+ });
349
+ if (tab === "insights") return effectiveInsightsIndexFilters({
350
+ domain: searchParams.get("domain")?.trim() ?? undefined,
351
+ });
352
+ return effectiveAssignmentsIndexFilters({
353
+ status: searchParams.get("status")?.trim() ?? undefined,
354
+ type: searchParams.get("type")?.trim() ?? undefined,
355
+ view: searchParams.get("view")?.trim() ?? undefined,
356
+ });
357
+ }
358
+
359
+ function buildNewsroomIndexLocationUri(tab: NewsroomIndexTab, filters: Record<string, string>): string {
360
+ const segments = [tab, "index"];
361
+ if (tab === "references") {
362
+ const normalized = filters as ReferencesIndexFilters;
363
+ if (normalized.status && normalized.status !== "exclude-pending") segments.push("status", normalized.status);
364
+ if (normalized.processing?.trim()) segments.push("processing", normalized.processing.trim());
365
+ if (normalized.order === "imported") segments.push("order", "imported");
366
+ } else if (tab === "messages") {
367
+ const normalized = filters as MessagesIndexFilters;
368
+ if (normalized.kind?.trim()) segments.push("kind", normalized.kind.trim());
369
+ if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
370
+ } else if (tab === "insights") {
371
+ const normalized = filters as InsightsIndexFilters;
372
+ if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
373
+ } else {
374
+ const normalized = filters as AssignmentsIndexFilters;
375
+ if (normalized.status?.trim()) segments.push("status", normalized.status.trim());
376
+ if (normalized.type?.trim()) segments.push("type", normalized.type.trim());
377
+ if (normalized.view === "budget") segments.push("view", "budget");
378
+ }
379
+ return `papyrus://newsroom/${segments.map((segment) => encodeURIComponent(segment)).join("/")}`;
380
+ }
381
+
382
+ function newsroomIndexUriToWebPath(
383
+ papyrusLocationUri: string,
384
+ objectId: string,
385
+ ): { ok: true; webPath: string; papyrusLocationUri: string; viewMode: "index"; indexFilters: Record<string, string> } | null {
386
+ const parsed = parseIndexUriTail(objectId);
387
+ if (!parsed) return null;
388
+ const { tab, filters } = parsed;
389
+ return {
390
+ ok: true,
391
+ papyrusLocationUri,
392
+ webPath: buildNewsroomIndexWebPath(tab, filters as ReferencesIndexFilters & MessagesIndexFilters & AssignmentsIndexFilters),
393
+ viewMode: "index",
394
+ indexFilters: filters,
395
+ };
396
+ }
397
+
398
+ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters: Record<string, string> } | null {
399
+ const segments = objectId.split("/").filter(Boolean);
400
+ if (!segments.length) return null;
401
+ const tab = segments[0];
402
+ if (tab !== "references" && tab !== "messages" && tab !== "assignments" && tab !== "insights") return null;
403
+ if (segments.length === 1) {
404
+ return { tab, filters: readIndexFiltersFromSearchParams(tab, new URLSearchParams()) };
405
+ }
406
+ if (segments[1] === "budget" && tab === "assignments") {
407
+ return { tab, filters: effectiveAssignmentsIndexFilters({ view: "budget" }) };
408
+ }
409
+ if (segments[1] !== "index") return null;
410
+ const filters: Record<string, string> = {};
411
+ for (let index = 2; index + 1 < segments.length; index += 2) {
412
+ filters[segments[index]] = decodeURIComponent(segments[index + 1]);
413
+ }
414
+ if (tab === "references") return { tab, filters: effectiveReferencesIndexFilters(filters) };
415
+ if (tab === "messages") return { tab, filters: effectiveMessagesIndexFilters(filters) };
416
+ if (tab === "insights") return { tab, filters: effectiveInsightsIndexFilters(filters) };
417
+ return { tab, filters: effectiveAssignmentsIndexFilters(filters) };
418
+ }
419
+
420
+ function normalizeWebPath(webPath: string): string {
421
+ const raw = webPath.trim();
422
+ if (!raw) return "/newsroom";
423
+ if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
424
+ const queryIndex = raw.indexOf("?");
425
+ if (queryIndex >= 0) return raw;
426
+ return raw;
427
+ }
428
+
429
+ function matchEditionArticlePath(pathname: string): { year: string; month: string; day: string; slug: string } | null {
430
+ const segments = pathname.split("/").filter(Boolean);
431
+ if (segments.length !== 4) return null;
432
+ const [year, month, day, slug] = segments;
433
+ if (!/^\d+$/.test(year) || !/^\d+$/.test(month) || !/^\d+$/.test(day)) return null;
434
+ return { year, month, day, slug: decodeURIComponent(slug) };
435
+ }
436
+
437
+ function searchAnchorUri(searchParams: URLSearchParams): string | null {
438
+ const anchorKind = canonicalObjectKind(searchParams.get("anchorKind") ?? "");
439
+ const anchorId = searchParams.get("anchorId")?.trim() ?? "";
440
+ if (!anchorKind || !anchorId) return null;
441
+ return `papyrus://${anchorKind}/${encodeURIComponent(anchorId)}`;
442
+ }
443
+
444
+ function objectKindToWebPath(kind: PapyrusObjectKind, objectId: string): string {
445
+ const encoded = encodeURIComponent(decodeURIComponent(objectId));
446
+ if (kind === "reference") return `/newsroom/references/${encoded}`;
447
+ if (kind === "message") return `/newsroom/messages/${encoded}`;
448
+ if (kind === "assignment") return `/newsroom/assignments/${encoded}`;
449
+ if (kind === "category") return `/newsroom/topics?category=${encoded}`;
450
+ if (kind === "semanticNode") return `/newsroom/concepts?node=${encoded}`;
451
+ if (kind === "newsroomSection") return `/newsroom/sections/${encoded}`;
452
+ if (kind === "item") return `/articles/${encoded}`;
453
+ return `/newsroom?object=${encodeURIComponent(kind)}:${encoded}`;
454
+ }
455
+
456
+ function parsePapyrusUri(uri: string): { kind: string; id: string } {
457
+ const parsed = new URL(uri);
458
+ if (parsed.protocol !== "papyrus:") throw new Error(`Papyrus URI must use papyrus:// scheme: ${uri}`);
459
+ if (parsed.search || parsed.hash) throw new Error(`Papyrus URI must not include query or fragment: ${uri}`);
460
+ const kind = canonicalObjectKind(decodeURIComponent(parsed.hostname));
461
+ const id = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
462
+ if (!kind) throw new Error(`Papyrus URI kind is required: ${uri}`);
463
+ if (!id) throw new Error(`Papyrus URI object id is required: ${uri}`);
464
+ return { kind, id };
465
+ }
466
+
467
+ function canonicalObjectKind(kind: string): string | null {
468
+ const trimmed = kind.trim();
469
+ if (!trimmed) return null;
470
+ if (trimmed === "newsroom" || trimmed === "site") return trimmed;
471
+ if (PAPYRUS_OBJECT_KINDS.has(trimmed as PapyrusObjectKind)) return trimmed;
472
+ const lowered = trimmed.toLowerCase();
473
+ for (const candidate of PAPYRUS_OBJECT_KINDS) {
474
+ if (candidate.toLowerCase() === lowered) return candidate;
475
+ }
476
+ if (lowered === "newsroomsection") return "newsroomSection";
477
+ if (lowered === "semanticnode") return "semanticNode";
478
+ if (lowered === "categoryset") return "categorySet";
479
+ return null;
480
+ }
@@ -0,0 +1,167 @@
1
+ import type { Article } from "./articles";
2
+ import type { EditionContent } from "./content-types";
3
+ import { normalizeEditionLayoutPlan, type EditionLayoutPlan } from "./layout-plan";
4
+ import { parseMarkusDocument } from "./markus-ir";
5
+ import { markusDocumentToArticle } from "./markus-to-article";
6
+ import { articleToPublicationItem } from "./publication-items";
7
+ import pilobolSampleAst from "../scripts/fixtures/pilobol-sample.ast.json";
8
+
9
+ export const PILOBOL_SAMPLE_SCENARIO_ID = "pilobol-sample";
10
+ export const PILOBOL_SAMPLE_SLUG = "sample";
11
+ export const PILOBOL_SAMPLE_MARKDOWN_PATH = "web/content/articles/sample.md";
12
+
13
+ let cachedArticle: Article | null = null;
14
+
15
+ /** Lazy singleton — parses the committed AST fixture once per process, never spawns `markus ast`. */
16
+ export function getPilobolSampleArticle(): Article {
17
+ if (!cachedArticle) {
18
+ const document = parseMarkusDocument(pilobolSampleAst);
19
+ cachedArticle = markusDocumentToArticle(document, {
20
+ slug: PILOBOL_SAMPLE_SLUG,
21
+ markdownDir: "web/content/articles",
22
+ assetUrlPrefix: "/pilobol-sample-assets",
23
+ section: "Pilobol.us",
24
+ });
25
+ }
26
+ return cachedArticle;
27
+ }
28
+
29
+ /**
30
+ * Newsprint layout for the single-article pilobol-sample scenario. Cannot use
31
+ * `createDefaultEditionLayoutPlan` — that helper hardcodes continuation frames
32
+ * for fixture slugs (`agent-procedure-patterns`, etc.) on pages 2–3.
33
+ */
34
+ export function createPilobolSampleLayoutPlan(): EditionLayoutPlan {
35
+ const slug = PILOBOL_SAMPLE_SLUG;
36
+ return normalizeEditionLayoutPlan(
37
+ {
38
+ pages: [
39
+ {
40
+ id: "page-1",
41
+ pageNumber: 1,
42
+ presetId: "front.mosaic",
43
+ grid: { columns: { min: 1, preferred: 6, max: 6 } },
44
+ regions: [
45
+ {
46
+ id: "pilobol-sample-front",
47
+ type: "fullPage",
48
+ localGrid: { columns: { min: 1, preferred: 6, max: 6 } },
49
+ blocks: [
50
+ {
51
+ id: `front-${slug}`,
52
+ type: "articleFrame",
53
+ presetId: "front.teaser",
54
+ itemId: slug,
55
+ flowKey: slug,
56
+ startCursor: "beginning",
57
+ role: "primary",
58
+ editorialPriority: "primary",
59
+ typography: { headlineScale: "feature" },
60
+ span: { min: 1, preferred: 6, max: 6 },
61
+ localGrid: { columns: { min: 1, preferred: 6, max: 6 } },
62
+ cutPolicy: { maxBodyLines: 16, jumpTargetPage: 2 },
63
+ media: [
64
+ {
65
+ required: false,
66
+ assetRole: "lead",
67
+ placement: {
68
+ anchor: "right",
69
+ span: { min: 1, preferred: 2, max: 2 },
70
+ vertical: "top",
71
+ collapse: "inline",
72
+ crop: "preserve",
73
+ wrapsText: true,
74
+ },
75
+ },
76
+ ],
77
+ pullQuote: {
78
+ required: false,
79
+ placements: [
80
+ {
81
+ anchor: "right",
82
+ span: { min: 1, preferred: 1, max: 2 },
83
+ vertical: "middle",
84
+ collapse: "omit",
85
+ crop: "preserve",
86
+ wrapsText: true,
87
+ },
88
+ ],
89
+ },
90
+ },
91
+ ],
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ id: "page-2",
97
+ pageNumber: 2,
98
+ presetId: "page.full",
99
+ grid: { columns: { min: 1, preferred: 6, max: 6 } },
100
+ regions: [
101
+ {
102
+ id: "pilobol-sample-continuation",
103
+ type: "fullPage",
104
+ size: { shrinkToContent: true },
105
+ localGrid: { columns: { min: 1, preferred: 6, max: 6 } },
106
+ blocks: [
107
+ {
108
+ id: `${slug}-page-2`,
109
+ type: "articleFrame",
110
+ presetId: "article.mediaInset",
111
+ itemId: slug,
112
+ flowKey: slug,
113
+ startCursor: "current",
114
+ role: "primary",
115
+ localGrid: { columns: { min: 2, preferred: 6, max: 6 } },
116
+ media: [
117
+ {
118
+ required: false,
119
+ assetRole: "continuationInset",
120
+ placement: {
121
+ anchor: "center",
122
+ span: { min: 1, preferred: 2, max: 3 },
123
+ vertical: "upperThird",
124
+ collapse: "inline",
125
+ crop: "preserve",
126
+ wrapsText: true,
127
+ },
128
+ },
129
+ ],
130
+ pullQuote: {
131
+ required: false,
132
+ placements: [
133
+ {
134
+ anchor: "right",
135
+ span: { min: 1, preferred: 1, max: 2 },
136
+ vertical: "middle",
137
+ collapse: "omit",
138
+ crop: "preserve",
139
+ wrapsText: true,
140
+ },
141
+ ],
142
+ },
143
+ },
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ },
150
+ "pilobol-sample layoutPlan",
151
+ );
152
+ }
153
+
154
+ export function createPilobolSampleEditionContent(): Pick<
155
+ EditionContent,
156
+ "title" | "editionDate" | "description" | "layoutPlan" | "items"
157
+ > {
158
+ const article = getPilobolSampleArticle();
159
+ return {
160
+ title: "Pilobol.us Dual Renderer PoC",
161
+ editionDate: article.dateline || "Sunday, September 6, 2026",
162
+ description:
163
+ "PPY-10e4cd: one Markus Markdown article projected into Pretext newsprint via lib/markus-projection.ts.",
164
+ layoutPlan: createPilobolSampleLayoutPlan(),
165
+ items: [articleToPublicationItem(article)],
166
+ };
167
+ }