@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,118 @@
1
+ import type { PapyrusWebUiContext } from "./papyrus-web-locations";
2
+
3
+ export const CONSOLE_WEB_CONTEXT_MESSAGE_KIND = "console_web_context";
4
+
5
+ export type ConsoleWebContextEvent = "session_start" | "navigation";
6
+
7
+ export type FormattedConsoleWebContext = {
8
+ visibleContent: string;
9
+ summary: string;
10
+ agentInstructions: string;
11
+ };
12
+
13
+ export function webUiContextKey(webUi: PapyrusWebUiContext): string {
14
+ return [
15
+ webUi.papyrusLocationUri,
16
+ webUi.webPath,
17
+ webUi.papyrusObjectUri ?? "",
18
+ webUi.viewMode ?? "",
19
+ ].join("|");
20
+ }
21
+
22
+ export function formatConsoleWebContextMessage(
23
+ webUi: PapyrusWebUiContext,
24
+ event: ConsoleWebContextEvent,
25
+ ): FormattedConsoleWebContext {
26
+ const locationUri = webUi.papyrusLocationUri.trim();
27
+ const webPath = webUi.webPath.trim();
28
+ const objectUri = webUi.papyrusObjectUri?.trim() ?? "";
29
+ const label = webUi.label?.trim() ?? "";
30
+ const tab = webUi.newsroomTab?.trim() ?? "";
31
+ const viewMode = webUi.viewMode?.trim() ?? "";
32
+
33
+ const headline = event === "session_start"
34
+ ? "Console session started on this page."
35
+ : "You navigated to a new page in the newsroom.";
36
+
37
+ const visibleLines = [
38
+ headline,
39
+ "",
40
+ locationUri ? `Papyrus location: ${locationUri}` : null,
41
+ webPath ? `Web path: ${webPath}` : null,
42
+ objectUri ? `Focused object: ${objectUri}` : null,
43
+ label ? `Label: ${label}` : null,
44
+ tab ? `Desk: ${tab}` : null,
45
+ viewMode ? `View: ${viewMode}` : null,
46
+ ].filter((line): line is string => Boolean(line));
47
+
48
+ const agentLines = [
49
+ "Papyrus web UI context for this console thread:",
50
+ locationUri ? `- papyrus_location_uri: ${locationUri}` : null,
51
+ webPath ? `- web_path: ${webPath}` : null,
52
+ objectUri ? `- focused_object_uri: ${objectUri}` : null,
53
+ label ? `- label: ${label}` : null,
54
+ viewMode ? `- view_mode: ${viewMode}` : null,
55
+ tab ? `- newsroom_tab: ${tab}` : null,
56
+ "",
57
+ "Use execute_tactus with papyrus.web.current_location{} to re-read this snapshot.",
58
+ "Use papyrus.web.navigate{ uri = \"papyrus://...\" } to move the browser when appropriate.",
59
+ ].filter((line): line is string => line !== null);
60
+
61
+ if (objectUri.startsWith("papyrus://reference/")) {
62
+ const referenceId = objectUri.slice("papyrus://reference/".length).trim();
63
+ if (referenceId) {
64
+ agentLines.push(
65
+ "",
66
+ `The user is viewing reference detail (${referenceId}). For questions about "this reference", the open page, or what they are looking at, do not ask them to paste the reference.`,
67
+ `First call execute_tactus with: return papyrus.Reference.get{ id = "${referenceId}" } (lineage ids from the URL resolve to the current version).`,
68
+ `Then call: return papyrus.knowledge.query{ semanticQuery = "Summarize the main claims, methods, and results.", anchors = { { uri = "papyrus://reference/${referenceId}" } } }`,
69
+ "Use double-quoted Lua strings only. Do not use JS parentheses, string.match, or multi-line Lua beyond simple return statements.",
70
+ );
71
+ }
72
+ }
73
+
74
+ const summaryParts = [label, tab, viewMode === "detail" ? "detail" : "index"].filter(Boolean);
75
+ return {
76
+ visibleContent: visibleLines.join("\n"),
77
+ summary: summaryParts.length
78
+ ? `Page context: ${summaryParts.join(" · ")}`
79
+ : "Page context updated",
80
+ agentInstructions: agentLines.join("\n"),
81
+ };
82
+ }
83
+
84
+ export function readConsoleAgentInstructionsFromMessageMetadata(metadata: unknown): string | null {
85
+ if (!metadata || typeof metadata !== "object") return null;
86
+ const record = metadata as Record<string, unknown>;
87
+ const consoleMeta = record.console;
88
+ if (!consoleMeta || typeof consoleMeta !== "object") return null;
89
+ const instructions = (consoleMeta as Record<string, unknown>).agentInstructions;
90
+ return typeof instructions === "string" && instructions.trim() ? instructions.trim() : null;
91
+ }
92
+
93
+ export function readConsoleWebUiFromMessageMetadata(metadata: unknown): PapyrusWebUiContext | null {
94
+ if (!metadata || typeof metadata !== "object") return null;
95
+ const record = metadata as Record<string, unknown>;
96
+ const consoleMeta = record.console;
97
+ if (!consoleMeta || typeof consoleMeta !== "object") return null;
98
+ const webUi = (consoleMeta as Record<string, unknown>).webUi;
99
+ if (!webUi || typeof webUi !== "object") return null;
100
+ const entry = webUi as Record<string, unknown>;
101
+ const webPath = typeof entry.webPath === "string" ? entry.webPath : "";
102
+ const papyrusLocationUri = typeof entry.papyrusLocationUri === "string" ? entry.papyrusLocationUri : "";
103
+ if (!webPath && !papyrusLocationUri) return null;
104
+ return {
105
+ webPath,
106
+ papyrusLocationUri,
107
+ papyrusObjectUri: typeof entry.papyrusObjectUri === "string" ? entry.papyrusObjectUri : null,
108
+ newsroomTab: typeof entry.newsroomTab === "string" ? entry.newsroomTab : null,
109
+ viewMode: entry.viewMode === "index" || entry.viewMode === "detail" ? entry.viewMode : null,
110
+ indexFilters: entry.indexFilters && typeof entry.indexFilters === "object"
111
+ ? Object.fromEntries(
112
+ Object.entries(entry.indexFilters as Record<string, unknown>)
113
+ .filter(([, value]) => typeof value === "string" && value.trim()),
114
+ ) as Record<string, string>
115
+ : null,
116
+ label: typeof entry.label === "string" ? entry.label : null,
117
+ };
118
+ }
@@ -0,0 +1,98 @@
1
+ import { generateClient } from "aws-amplify/data";
2
+ import type { Schema } from "../amplify/data/resource";
3
+
4
+ export type ContentActionError = { code: string; message: string; line: number | null };
5
+
6
+ export type DeriveMarkusInput = { frontMatterYaml: string | null; bodyMarkus: string; includeIr?: boolean };
7
+ export type SaveItemDraftInput = {
8
+ id: string | null;
9
+ type: string;
10
+ slug: string;
11
+ section: string | null;
12
+ frontMatterYaml: string | null;
13
+ bodyMarkus: string;
14
+ aliases: string[];
15
+ expectedContentHash: string | null;
16
+ };
17
+
18
+ export type DeriveMarkusResult = { ok: true; bodyIrBytes: number; bodyIr?: unknown; errors: [] };
19
+ export type SavedItemSummary = {
20
+ id: string;
21
+ contentHash: string;
22
+ status: string;
23
+ slug: string;
24
+ versionNumber: number | null;
25
+ };
26
+ export type SaveItemDraftResult = { ok: true; item: SavedItemSummary };
27
+ export type PublishItemResult = { ok: true; changed: boolean; publishedId: string; versionNumber: number | null };
28
+ export type UnpublishItemResult = { ok: true; changed: boolean };
29
+
30
+ export class ContentActionFailure extends Error {
31
+ readonly errors: ContentActionError[];
32
+
33
+ constructor(errors: ContentActionError[]) {
34
+ super(errors.map((error) => error.message).join("; ") || "Content action failed");
35
+ this.name = "ContentActionFailure";
36
+ this.errors = errors;
37
+ }
38
+ }
39
+
40
+ type GraphqlEnvelope = { data?: unknown; errors?: { message?: string }[] | null };
41
+
42
+ export type ContentActionsTransport = {
43
+ queries: { deriveMarkus: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope> };
44
+ mutations: {
45
+ saveItemDraft: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope>;
46
+ publishItem: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope>;
47
+ unpublishItem: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope>;
48
+ };
49
+ };
50
+
51
+ const USER_POOL = { authMode: "userPool" } as const;
52
+
53
+ function parseActionResponse<T>(envelope: GraphqlEnvelope): T {
54
+ if (envelope.errors && envelope.errors.length > 0) {
55
+ throw new Error(envelope.errors.map((error) => error.message ?? "GraphQL error").join("; "));
56
+ }
57
+ const raw = envelope.data;
58
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
59
+ if (!parsed || typeof parsed !== "object") {
60
+ throw new Error("The content action returned no data.");
61
+ }
62
+ const response = parsed as { ok?: boolean; errors?: ContentActionError[] };
63
+ if (response.ok !== true) {
64
+ throw new ContentActionFailure(response.errors ?? []);
65
+ }
66
+ return parsed as T;
67
+ }
68
+
69
+ export function createContentActionsClient(transport: ContentActionsTransport) {
70
+ return {
71
+ async deriveMarkus(input: DeriveMarkusInput): Promise<DeriveMarkusResult> {
72
+ return parseActionResponse(await transport.queries.deriveMarkus({ input: JSON.stringify(input) }, USER_POOL));
73
+ },
74
+ async saveItemDraft(input: SaveItemDraftInput): Promise<SaveItemDraftResult> {
75
+ return parseActionResponse(await transport.mutations.saveItemDraft({ input: JSON.stringify(input) }, USER_POOL));
76
+ },
77
+ async publishItem(id: string): Promise<PublishItemResult> {
78
+ return parseActionResponse(await transport.mutations.publishItem({ input: JSON.stringify({ id }) }, USER_POOL));
79
+ },
80
+ async unpublishItem(id: string): Promise<UnpublishItemResult> {
81
+ return parseActionResponse(await transport.mutations.unpublishItem({ input: JSON.stringify({ id }) }, USER_POOL));
82
+ },
83
+ };
84
+ }
85
+
86
+ let defaultClient: ReturnType<typeof createContentActionsClient> | null = null;
87
+
88
+ function contentActionsClient() {
89
+ if (!defaultClient) {
90
+ defaultClient = createContentActionsClient(generateClient<Schema>() as unknown as ContentActionsTransport);
91
+ }
92
+ return defaultClient;
93
+ }
94
+
95
+ export const deriveMarkus = (input: DeriveMarkusInput) => contentActionsClient().deriveMarkus(input);
96
+ export const saveItemDraft = (input: SaveItemDraftInput) => contentActionsClient().saveItemDraft(input);
97
+ export const publishItem = (id: string) => contentActionsClient().publishItem(id);
98
+ export const unpublishItem = (id: string) => contentActionsClient().unpublishItem(id);
@@ -0,0 +1,190 @@
1
+ import { ContentActionFailure } from "./content-actions-client";
2
+ import type {
3
+ DeriveMarkusInput,
4
+ DeriveMarkusResult,
5
+ PublishItemResult,
6
+ SaveItemDraftInput,
7
+ SaveItemDraftResult,
8
+ UnpublishItemResult,
9
+ } from "./content-actions-client";
10
+ import {
11
+ createDemoArticleRecords,
12
+ demoContentHash,
13
+ type DemoArticleRecord,
14
+ } from "./newsroom-demo-articles";
15
+ import { sourceLineOffset, type ArticlesBackend, type EditableArticle, type NewsroomArticleRow } from "./newsroom-articles";
16
+
17
+ function titleFromYaml(frontMatterYaml: string | null, fallback: string): string {
18
+ for (const line of (frontMatterYaml ?? "").split("\n")) {
19
+ const match = /^title\s*:\s*(.*)$/.exec(line);
20
+ if (match && match[1].trim()) return match[1].trim().replace(/^["']|["']$/g, "");
21
+ }
22
+ return fallback;
23
+ }
24
+
25
+ const DEMO_IMAGE_DIRECTIVE = /^::image\{([^}]*)\}$/;
26
+
27
+ function demoAttribute(attributes: string, name: string): string {
28
+ const match = new RegExp(`${name}="([^"]*)"`).exec(attributes);
29
+ return match ? match[1] : "";
30
+ }
31
+
32
+ function demoEnvelope(bodyMarkus: string) {
33
+ const images: Record<string, Record<string, unknown>> = {};
34
+ const children = bodyMarkus
35
+ .split(/\n{2,}/)
36
+ .map((block) => block.trim())
37
+ .filter(Boolean)
38
+ .map((block) => {
39
+ const heading = /^(#{1,6})\s+(.*)$/.exec(block);
40
+ if (heading) {
41
+ return { type: "heading", level: heading[1].length, inline: [{ type: "text", text: heading[2] }], line: null };
42
+ }
43
+ const image = DEMO_IMAGE_DIRECTIVE.exec(block);
44
+ if (image) {
45
+ const token = `PAPYRUSMARKUP${String(Object.keys(images).length + 1).padStart(5, "0")}END`;
46
+ images[token] = {
47
+ src: demoAttribute(image[1], "src"),
48
+ alt: demoAttribute(image[1], "alt"),
49
+ layout: demoAttribute(image[1], "layout") || null,
50
+ };
51
+ return { type: "paragraph", inline: [{ type: "text", text: token }], line: null };
52
+ }
53
+ return { type: "paragraph", inline: [{ type: "text", text: block }], line: null };
54
+ });
55
+ return {
56
+ schemaVersion: 1,
57
+ markus: { version: "demo", irSchemaVersion: 1 },
58
+ document: { type: "document", schema_version: 1, front_matter: {}, children },
59
+ papyrus: { images, citations: {}, citationLists: {}, entries: {}, bibliography: [] },
60
+ };
61
+ }
62
+
63
+ export function createDemoArticlesBackend(): ArticlesBackend {
64
+ const records = new Map<string, DemoArticleRecord>(createDemoArticleRecords().map((record) => [record.id, record]));
65
+ let created = 0;
66
+
67
+ const requireRecord = (id: string): DemoArticleRecord => {
68
+ const record = records.get(id);
69
+ if (!record) throw new ContentActionFailure([{ code: "not-found", message: "That article was not found.", line: null }]);
70
+ return record;
71
+ };
72
+
73
+ return {
74
+ actions: {
75
+ async deriveMarkus(input: DeriveMarkusInput): Promise<DeriveMarkusResult> {
76
+ const lines = input.bodyMarkus.split("\n");
77
+ const invalidIndex = lines.findIndex((line) => line.includes(":::nope"));
78
+ if (invalidIndex >= 0) {
79
+ throw new ContentActionFailure([
80
+ { code: "markus-validation", message: 'Unknown directive "nope".', line: invalidIndex + 1 + sourceLineOffset(input.frontMatterYaml) },
81
+ ]);
82
+ }
83
+ const envelope = demoEnvelope(input.bodyMarkus);
84
+ return {
85
+ ok: true,
86
+ bodyIrBytes: JSON.stringify(envelope).length,
87
+ ...(input.includeIr ? { bodyIr: envelope } : {}),
88
+ errors: [],
89
+ };
90
+ },
91
+ async saveItemDraft(input: SaveItemDraftInput): Promise<SaveItemDraftResult> {
92
+ const existing = input.id ? requireRecord(input.id) : null;
93
+ if (existing && input.expectedContentHash !== null && input.expectedContentHash !== existing.contentHash) {
94
+ throw new ContentActionFailure([
95
+ { code: "conflict", message: "The item changed since it was loaded.", line: null },
96
+ ]);
97
+ }
98
+ if (existing && existing.everPublished && existing.slug !== input.slug) {
99
+ throw new ContentActionFailure([
100
+ { code: "slug-locked", message: "The slug cannot change once the item has been published.", line: null },
101
+ ]);
102
+ }
103
+ const id = existing?.id ?? `demo-article-new-${(created += 1)}`;
104
+ const title = titleFromYaml(input.frontMatterYaml, input.slug);
105
+ const frontMatterYaml = input.frontMatterYaml ?? "";
106
+ const record: DemoArticleRecord = {
107
+ id,
108
+ type: input.type,
109
+ slug: input.slug,
110
+ section: input.section,
111
+ title,
112
+ status: existing?.status ?? "draft",
113
+ frontMatterYaml,
114
+ bodyMarkus: input.bodyMarkus,
115
+ contentHash: demoContentHash(frontMatterYaml, input.bodyMarkus),
116
+ publishedContentHash: existing?.publishedContentHash ?? null,
117
+ versionNumber: existing?.versionNumber ?? null,
118
+ everPublished: existing?.everPublished ?? false,
119
+ updatedAt: new Date().toISOString(),
120
+ };
121
+ records.set(id, record);
122
+ return {
123
+ ok: true,
124
+ item: {
125
+ id,
126
+ contentHash: record.contentHash,
127
+ status: record.status,
128
+ slug: record.slug,
129
+ versionNumber: record.versionNumber,
130
+ },
131
+ };
132
+ },
133
+ async publishItem(id: string): Promise<PublishItemResult> {
134
+ const record = requireRecord(id);
135
+ const changed = record.status !== "published" || record.publishedContentHash !== record.contentHash;
136
+ const versionNumber = changed ? (record.versionNumber ?? 0) + 1 : record.versionNumber;
137
+ records.set(id, {
138
+ ...record,
139
+ status: "published",
140
+ everPublished: true,
141
+ publishedContentHash: record.contentHash,
142
+ versionNumber,
143
+ updatedAt: new Date().toISOString(),
144
+ });
145
+ return { ok: true, changed, publishedId: `published-${id}`, versionNumber };
146
+ },
147
+ async unpublishItem(id: string): Promise<UnpublishItemResult> {
148
+ const record = requireRecord(id);
149
+ const changed = record.status === "published";
150
+ records.set(id, {
151
+ ...record,
152
+ status: "draft",
153
+ publishedContentHash: null,
154
+ updatedAt: new Date().toISOString(),
155
+ });
156
+ return { ok: true, changed };
157
+ },
158
+ },
159
+ async listRows(): Promise<NewsroomArticleRow[]> {
160
+ return [...records.values()]
161
+ .map((record) => ({
162
+ id: record.id,
163
+ type: record.type,
164
+ slug: record.slug,
165
+ section: record.section,
166
+ title: record.title || record.slug,
167
+ status: record.status,
168
+ pending: record.status === "published" && record.publishedContentHash !== record.contentHash,
169
+ updatedAt: record.updatedAt,
170
+ }))
171
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
172
+ },
173
+ async loadArticle(id: string): Promise<EditableArticle> {
174
+ const record = requireRecord(id);
175
+ return {
176
+ id: record.id,
177
+ type: record.type,
178
+ section: record.section,
179
+ slug: record.slug,
180
+ frontMatterYaml: record.frontMatterYaml,
181
+ bodyMarkus: record.bodyMarkus,
182
+ contentHash: record.contentHash,
183
+ status: record.status,
184
+ versionNumber: record.versionNumber,
185
+ aliases: [],
186
+ everPublished: record.everPublished,
187
+ };
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,23 @@
1
+ type ContentLoadMetadata = Record<string, string | number | boolean | null | undefined>;
2
+
3
+ export function isContentLoadTimingEnabled(): boolean {
4
+ return process.env.PAPYRUS_CONTENT_LOAD_TIMING === "1";
5
+ }
6
+
7
+ export async function withContentLoadTiming<T>(
8
+ label: string,
9
+ metadata: ContentLoadMetadata,
10
+ work: () => Promise<T>,
11
+ ): Promise<T> {
12
+ if (!isContentLoadTimingEnabled()) {
13
+ return work();
14
+ }
15
+
16
+ const started = performance.now();
17
+ try {
18
+ return await work();
19
+ } finally {
20
+ const elapsedMs = Math.round(performance.now() - started);
21
+ console.info(`[papyrus-content-load] ${label} ${JSON.stringify({ ...metadata, ms: elapsedMs })}`);
22
+ }
23
+ }
@@ -0,0 +1,43 @@
1
+ import type { ContentRepository } from "./content-types";
2
+ import { graphqlContentRepository } from "./graphql-content-repository";
3
+ import { getLayoutScenario } from "./layout-scenarios";
4
+ import { assertContentSourceMatchesEnv } from "./site-env";
5
+
6
+ assertContentSourceMatchesEnv();
7
+
8
+ export const contentRepository: ContentRepository = {
9
+ loadEditionContent({ scenarioId, editionDate, editionSlug } = {}) {
10
+ if (scenarioId) return getLayoutScenario(scenarioId);
11
+ return graphqlContentRepository.loadEditionContent({ editionDate, editionSlug });
12
+ },
13
+ getLatestPublishedEdition() {
14
+ return graphqlContentRepository.getLatestPublishedEdition();
15
+ },
16
+ getFirstPublishedEdition() {
17
+ return graphqlContentRepository.getFirstPublishedEdition();
18
+ },
19
+ listPublishedEditions(options) {
20
+ return graphqlContentRepository.listPublishedEditions(options);
21
+ },
22
+ getArticle(slug: string) {
23
+ return graphqlContentRepository.getArticle(slug);
24
+ },
25
+ getEditionArticle(options) {
26
+ return graphqlContentRepository.getEditionArticle(options);
27
+ },
28
+ getEditionItem(options) {
29
+ return graphqlContentRepository.getEditionItem(options);
30
+ },
31
+ listArticleSlugs() {
32
+ return graphqlContentRepository.listArticleSlugs();
33
+ },
34
+ };
35
+
36
+ export function getScenarioIdParam(scenario: string | string[] | null | undefined): string | null {
37
+ if (Array.isArray(scenario)) return scenario[0] ?? null;
38
+ return scenario ?? null;
39
+ }
40
+
41
+ export function isGraphQLContentSource(): boolean {
42
+ return true;
43
+ }
@@ -0,0 +1,26 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { getContentSource, type ContentSource } from "./site-env";
3
+
4
+ export type ContentSourceContext = {
5
+ source: ContentSource;
6
+ /** Request-scoped data client (drafts mode: carries the visitor's session cookies). */
7
+ client?: unknown;
8
+ };
9
+
10
+ const contentSourceStorage = new AsyncLocalStorage<ContentSourceContext>();
11
+
12
+ export function runWithContentSource<T>(
13
+ source: ContentSource,
14
+ operation: () => Promise<T>,
15
+ client?: unknown,
16
+ ): Promise<T> {
17
+ return contentSourceStorage.run({ source, client }, operation);
18
+ }
19
+
20
+ export function currentRequestClient<T>(): T | undefined {
21
+ return contentSourceStorage.getStore()?.client as T | undefined;
22
+ }
23
+
24
+ export function currentContentSource(): ContentSource {
25
+ return contentSourceStorage.getStore()?.source ?? getContentSource();
26
+ }
@@ -0,0 +1,104 @@
1
+ import type { Article, ArticleVideoAsset } from "./articles";
2
+ import type { EditionLayoutPlan } from "./layout-plan";
3
+ import type { PublicationItem } from "./publication-items";
4
+
5
+ export type ContentSource = "scenario" | "graphql";
6
+
7
+ import type { PretextLayout } from "./renderer-config";
8
+
9
+ /** @deprecated Use PretextLayout from lib/renderer-config.ts */
10
+ export type EditionPresentationFormat = PretextLayout;
11
+
12
+ export type EditionSection = {
13
+ key: string;
14
+ label: string;
15
+ description?: string | null;
16
+ itemIds: string[];
17
+ };
18
+
19
+ export type NewsDeskCategoryTreeNode = {
20
+ id: string;
21
+ categorySetId: string;
22
+ categoryKey: string;
23
+ parentCategoryKey?: string | null;
24
+ displayName: string;
25
+ shortTitle?: string | null;
26
+ subtitle?: string | null;
27
+ description?: string | null;
28
+ status: string;
29
+ seedItemIds?: Array<string | null> | null;
30
+ holdoutItemIds?: Array<string | null> | null;
31
+ rank?: number | null;
32
+ depth?: number | null;
33
+ };
34
+
35
+ export type NewsDeskAppendix = {
36
+ categorySetId: string;
37
+ corpusId: string;
38
+ displayName: string;
39
+ description?: string | null;
40
+ generatedAt?: string | null;
41
+ nodes: NewsDeskCategoryTreeNode[];
42
+ };
43
+
44
+ export type EditionContent = {
45
+ id: string;
46
+ source: ContentSource;
47
+ title: string;
48
+ editionDate: string;
49
+ items: PublicationItem[];
50
+ sections: EditionSection[];
51
+ layoutPlan: EditionLayoutPlan;
52
+ placeholderMode?: "emptyEdition";
53
+ suppressNewsDeskAppendix?: boolean;
54
+ newsDeskAppendix?: NewsDeskAppendix | null;
55
+ scenarioId?: string;
56
+ description?: string;
57
+ editionVideo?: ArticleVideoAsset | null;
58
+ };
59
+
60
+ export type EditionRouteSummary = {
61
+ id: string;
62
+ slug: string;
63
+ title: string;
64
+ editionDate: string;
65
+ publishedAt?: string | null;
66
+ description?: string | null;
67
+ };
68
+
69
+ export type ListPublishedEditionsOptions = {
70
+ limit?: number;
71
+ nextToken?: string | null;
72
+ };
73
+
74
+ export type PublishedEditionConnection = {
75
+ editions: EditionRouteSummary[];
76
+ nextToken?: string | null;
77
+ };
78
+
79
+ export type LoadEditionContentOptions = {
80
+ scenarioId?: string | null;
81
+ editionDate?: string | null;
82
+ editionSlug?: string | null;
83
+ };
84
+
85
+ export type GetEditionArticleOptions = {
86
+ editionDate: string;
87
+ articleSlug: string;
88
+ };
89
+
90
+ export type GetEditionItemOptions = {
91
+ editionDate: string;
92
+ itemSlug: string;
93
+ };
94
+
95
+ export type ContentRepository = {
96
+ loadEditionContent(options?: LoadEditionContentOptions): EditionContent | Promise<EditionContent>;
97
+ getLatestPublishedEdition(): EditionRouteSummary | null | Promise<EditionRouteSummary | null>;
98
+ getFirstPublishedEdition(): EditionRouteSummary | null | Promise<EditionRouteSummary | null>;
99
+ listPublishedEditions(options?: ListPublishedEditionsOptions): PublishedEditionConnection | Promise<PublishedEditionConnection>;
100
+ getArticle(slug: string): Article | undefined | Promise<Article | undefined>;
101
+ getEditionArticle(options: GetEditionArticleOptions): Article | undefined | Promise<Article | undefined>;
102
+ getEditionItem(options: GetEditionItemOptions): PublicationItem | undefined | Promise<PublicationItem | undefined>;
103
+ listArticleSlugs(): string[] | Promise<string[]>;
104
+ };
@@ -0,0 +1,3 @@
1
+ export function defineSite(site) {
2
+ return site;
3
+ }
@@ -0,0 +1,21 @@
1
+ import type { PapyrusSiteBackendConfig } from "../amplify/site-backend-config";
2
+ import type { SiteBrand } from "./site-brand";
3
+
4
+ /**
5
+ * What a publication repo's `papyrus.config.ts` default-exports.
6
+ * Resolved by the `papyrus-site` alias (tsconfig `paths` in Papyrus itself,
7
+ * `withPapyrus()` in a publication repo). Also the argument of
8
+ * `defineSiteBackend(site)` so `ampx` never needs the brand graph.
9
+ */
10
+ export type PapyrusSite = {
11
+ /** Brands this publication registers (in addition to Papyrus's built-ins). */
12
+ brands?: SiteBrand[];
13
+ /** Brand used when PAPYRUS_SITE_BRAND is unset. */
14
+ defaultBrand?: string;
15
+ /** Options for `defineSiteBackend(site)` (Amplify Gen 2 backend). */
16
+ backend?: PapyrusSiteBackendConfig;
17
+ };
18
+
19
+ export function defineSite(site: PapyrusSite): PapyrusSite {
20
+ return site;
21
+ }
@@ -0,0 +1,15 @@
1
+ import { readAmplifyOutputsFile } from "./amplify-outputs-path";
2
+
3
+ const DEMO_MARKERS = [
4
+ "demo-user-pool-client-id-not-a-secret",
5
+ "da2-demoapikeynotasecret000000",
6
+ "00000000-demo-4000-8000-000000000000",
7
+ ] as const;
8
+
9
+ /** True when amplify_outputs.json is the offline demo stub (see amplify/fixtures/demo-amplify-outputs.json). */
10
+ export function isDemoAmplifyOutputs(): boolean {
11
+ const outputs = readAmplifyOutputsFile();
12
+ if (!outputs) return false;
13
+ const serialized = JSON.stringify(outputs);
14
+ return DEMO_MARKERS.some((marker) => serialized.includes(marker));
15
+ }
@@ -0,0 +1,18 @@
1
+ export type DevSandboxEditorAuth = {
2
+ username: string;
3
+ password: string;
4
+ };
5
+
6
+ const DEFAULT_SEED_USERNAME = "papyrus-seed-editor@example.com";
7
+ const DEFAULT_SEED_PASSWORD = "PapyrusSeed1!";
8
+
9
+ export function resolveDevSandboxEditorAuth(): DevSandboxEditorAuth | null {
10
+ if (process.env.NODE_ENV !== "development") return null;
11
+ if (process.env.PAPYRUS_DEV_SEED_SIGNIN === "0") return null;
12
+
13
+ const username = process.env.PAPYRUS_SEED_USERNAME?.trim() || DEFAULT_SEED_USERNAME;
14
+ const password = process.env.PAPYRUS_SEED_PASSWORD?.trim() || DEFAULT_SEED_PASSWORD;
15
+ if (!username || !password) return null;
16
+
17
+ return { username, password };
18
+ }