@anthusai/papyrus 0.0.0 → 1.0.0-next.1

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,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,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,40 @@
1
+ import type { ContentRepository } from "./content-types";
2
+ import { graphqlContentRepository } from "./graphql-content-repository";
3
+ import { getLayoutScenario } from "./layout-scenarios";
4
+
5
+ export const contentRepository: ContentRepository = {
6
+ loadEditionContent({ scenarioId, editionDate, editionSlug } = {}) {
7
+ if (scenarioId) return getLayoutScenario(scenarioId);
8
+ return graphqlContentRepository.loadEditionContent({ editionDate, editionSlug });
9
+ },
10
+ getLatestPublishedEdition() {
11
+ return graphqlContentRepository.getLatestPublishedEdition();
12
+ },
13
+ getFirstPublishedEdition() {
14
+ return graphqlContentRepository.getFirstPublishedEdition();
15
+ },
16
+ listPublishedEditions(options) {
17
+ return graphqlContentRepository.listPublishedEditions(options);
18
+ },
19
+ getArticle(slug: string) {
20
+ return graphqlContentRepository.getArticle(slug);
21
+ },
22
+ getEditionArticle(options) {
23
+ return graphqlContentRepository.getEditionArticle(options);
24
+ },
25
+ getEditionItem(options) {
26
+ return graphqlContentRepository.getEditionItem(options);
27
+ },
28
+ listArticleSlugs() {
29
+ return graphqlContentRepository.listArticleSlugs();
30
+ },
31
+ };
32
+
33
+ export function getScenarioIdParam(scenario: string | string[] | null | undefined): string | null {
34
+ if (Array.isArray(scenario)) return scenario[0] ?? null;
35
+ return scenario ?? null;
36
+ }
37
+
38
+ export function isGraphQLContentSource(): boolean {
39
+ return true;
40
+ }
@@ -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
+ }
@@ -0,0 +1,128 @@
1
+ export const DOCTRINE_ITEM_TYPE = "doctrine";
2
+ export const DOCTRINE_ITEM_STATUS = "private";
3
+ export const DOCTRINE_ITEM_TYPE_STATUS = "doctrine#private";
4
+
5
+ export type DoctrineKind = "mission" | "policy";
6
+ export type DoctrineScope = "publication" | "category";
7
+
8
+ export type DoctrineDefinition = {
9
+ kind: DoctrineKind;
10
+ label: string;
11
+ slug: string;
12
+ id: string;
13
+ lineageId: string;
14
+ };
15
+
16
+ export type DoctrineCategory = {
17
+ categorySetId: string;
18
+ categoryKey: string;
19
+ displayName: string;
20
+ shortTitle?: string | null;
21
+ rank?: number | null;
22
+ depth?: number | null;
23
+ lineageId?: string | null;
24
+ id?: string | null;
25
+ };
26
+
27
+ export const DOCTRINE_DEFINITIONS: DoctrineDefinition[] = [
28
+ {
29
+ kind: "mission",
30
+ label: "Editorial Mission",
31
+ slug: "editorial-doctrine-mission",
32
+ id: "item-editorial-doctrine-mission-v1",
33
+ lineageId: "item-editorial-doctrine-mission",
34
+ },
35
+ {
36
+ kind: "policy",
37
+ label: "Editorial Policy",
38
+ slug: "editorial-doctrine-policy",
39
+ id: "item-editorial-doctrine-policy-v1",
40
+ lineageId: "item-editorial-doctrine-policy",
41
+ },
42
+ ];
43
+
44
+ export const DOCTRINE_DEFINITION_BY_KIND = new Map(
45
+ DOCTRINE_DEFINITIONS.map((definition) => [definition.kind, definition]),
46
+ );
47
+
48
+ export function buildCategoryDoctrineDefinition(category: DoctrineCategory, kind: DoctrineKind): DoctrineDefinition {
49
+ const safeCategoryKey = safeDoctrineKey(category.categoryKey);
50
+ const categoryLineageId = category.lineageId ?? category.id ?? category.categoryKey;
51
+ const safeCategoryLineageId = safeDoctrineKey(categoryLineageId);
52
+ const label = kind === "mission" ? "Category Mission" : "Category Policies";
53
+ return {
54
+ kind,
55
+ label,
56
+ slug: `desk-doctrine-${safeCategoryKey}-${kind}`,
57
+ id: `item-desk-doctrine-${safeCategoryLineageId}-${kind}-v1`,
58
+ lineageId: `item-desk-doctrine-${safeCategoryLineageId}-${kind}`,
59
+ };
60
+ }
61
+
62
+ export function getCategoryDoctrineDefinitions(category: DoctrineCategory): DoctrineDefinition[] {
63
+ return [
64
+ buildCategoryDoctrineDefinition(category, "mission"),
65
+ buildCategoryDoctrineDefinition(category, "policy"),
66
+ ];
67
+ }
68
+
69
+ export function categoryDoctrineEditorialValue(category: DoctrineCategory, kind: DoctrineKind): string {
70
+ return JSON.stringify({
71
+ scope: "category",
72
+ kind,
73
+ categorySetId: category.categorySetId,
74
+ categoryLineageId: category.lineageId ?? category.id ?? category.categoryKey,
75
+ categoryKey: category.categoryKey,
76
+ categoryDepth: category.depth ?? null,
77
+ });
78
+ }
79
+
80
+ export function buildDeskDoctrineDefinition(category: DoctrineCategory, kind: DoctrineKind): DoctrineDefinition {
81
+ return buildCategoryDoctrineDefinition(category, kind);
82
+ }
83
+
84
+ export function getDeskDoctrineDefinitions(category: DoctrineCategory): DoctrineDefinition[] {
85
+ return getCategoryDoctrineDefinitions(category);
86
+ }
87
+
88
+ export function deskDoctrineEditorialValue(category: DoctrineCategory, kind: DoctrineKind): string {
89
+ return categoryDoctrineEditorialValue(category, kind);
90
+ }
91
+
92
+ export function isDeskDoctrineSlug(slug: string | null | undefined): boolean {
93
+ return typeof slug === "string" && slug.startsWith("desk-doctrine-");
94
+ }
95
+
96
+ export function doctrineBodyToText(body: Array<string | null> | null | undefined): string {
97
+ return compactDoctrineParagraphs(body).join("\n\n");
98
+ }
99
+
100
+ export function doctrineTextToBody(text: string): string[] {
101
+ return normalizeDoctrineText(text)
102
+ .split(/\n\s*\n+/)
103
+ .map((paragraph) => paragraph.trim())
104
+ .filter(Boolean);
105
+ }
106
+
107
+ export function doctrineEditorialValue(kind: DoctrineKind): string {
108
+ return JSON.stringify({ kind });
109
+ }
110
+
111
+ function safeDoctrineKey(value: string): string {
112
+ return value
113
+ .trim()
114
+ .toLowerCase()
115
+ .replace(/[^a-z0-9]+/g, "-")
116
+ .replace(/^-+|-+$/g, "")
117
+ || "topic";
118
+ }
119
+
120
+ function normalizeDoctrineText(text: string): string {
121
+ return text.replace(/\r\n?/g, "\n").trim();
122
+ }
123
+
124
+ function compactDoctrineParagraphs(body: Array<string | null> | null | undefined): string[] {
125
+ return (body ?? [])
126
+ .map((paragraph) => (typeof paragraph === "string" ? paragraph.trim() : ""))
127
+ .filter(Boolean);
128
+ }
@@ -0,0 +1,155 @@
1
+ const MONTH_NAMES = [
2
+ "january",
3
+ "february",
4
+ "march",
5
+ "april",
6
+ "may",
7
+ "june",
8
+ "july",
9
+ "august",
10
+ "september",
11
+ "october",
12
+ "november",
13
+ "december",
14
+ ] as const;
15
+
16
+ type EditionDateRouteInput = {
17
+ year: string;
18
+ month: string;
19
+ day: string;
20
+ };
21
+
22
+ type ParsedEditionDateRoute = {
23
+ editionDate: string;
24
+ canonicalPath: string;
25
+ isCanonical: boolean;
26
+ };
27
+
28
+ type ParsedEditionPageRoute = ParsedEditionDateRoute & {
29
+ pageNumber: number;
30
+ };
31
+
32
+ type ParsedEditionArticleRoute = ParsedEditionDateRoute & {
33
+ articleSlug: string;
34
+ };
35
+
36
+ type ParsedEditionSectionRoute = ParsedEditionDateRoute & {
37
+ sectionKey: string;
38
+ };
39
+
40
+ const RESERVED_DATE_CHILD_SEGMENTS = new Set(["page", "section"]);
41
+
42
+ export function getEditionDatePath(editionDate: string): string {
43
+ const match = editionDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
44
+ if (!match) return "/";
45
+
46
+ const monthIndex = Number(match[2]) - 1;
47
+ const monthName = MONTH_NAMES[monthIndex];
48
+ if (!monthName) return "/";
49
+
50
+ return `/${match[1]}/${monthName}/${match[3]}`;
51
+ }
52
+
53
+ export function getEditionPagePath(editionDate: string, pageNumber: number): string {
54
+ const basePath = getEditionDatePath(editionDate);
55
+ return pageNumber <= 1 ? basePath : `${basePath}/page/${pageNumber}`;
56
+ }
57
+
58
+ export function getEditionSectionPath(editionDate: string, sectionKey: string): string {
59
+ return `${getEditionDatePath(editionDate)}/section/${encodeURIComponent(sectionKey)}`;
60
+ }
61
+
62
+ export function getEditionArticlePath(editionDate: string, articleSlug: string): string {
63
+ return `${getEditionDatePath(editionDate)}/${encodeURIComponent(articleSlug)}`;
64
+ }
65
+
66
+ export const getEditionItemPath = getEditionArticlePath;
67
+
68
+ export function parseEditionDateRoute(input: EditionDateRouteInput): ParsedEditionDateRoute | null {
69
+ const parsed = parseEditionDateSegments(input);
70
+ if (!parsed) return null;
71
+
72
+ return {
73
+ ...parsed,
74
+ isCanonical: getCurrentDatePath(input) === parsed.canonicalPath,
75
+ };
76
+ }
77
+
78
+ export function parseEditionPageRoute(input: EditionDateRouteInput & { pageNumber: string }): ParsedEditionPageRoute | null {
79
+ const parsed = parseEditionDateSegments(input);
80
+ if (!parsed) return null;
81
+ if (!/^\d+$/.test(input.pageNumber)) return null;
82
+
83
+ const pageNumber = Number(input.pageNumber);
84
+ if (!Number.isSafeInteger(pageNumber) || pageNumber < 1) return null;
85
+
86
+ const canonicalPath = getEditionPagePath(parsed.editionDate, pageNumber);
87
+ const currentPath = `${getCurrentDatePath(input)}/page/${input.pageNumber}`;
88
+ return {
89
+ editionDate: parsed.editionDate,
90
+ canonicalPath,
91
+ isCanonical: currentPath === canonicalPath,
92
+ pageNumber,
93
+ };
94
+ }
95
+
96
+ export function parseEditionArticleRoute(input: EditionDateRouteInput & { articleSlug: string }): ParsedEditionArticleRoute | null {
97
+ const parsed = parseEditionDateSegments(input);
98
+ if (!parsed) return null;
99
+ if (RESERVED_DATE_CHILD_SEGMENTS.has(input.articleSlug.toLowerCase())) return null;
100
+
101
+ const canonicalPath = getEditionArticlePath(parsed.editionDate, input.articleSlug);
102
+ const currentPath = `${getCurrentDatePath(input)}/${input.articleSlug}`;
103
+ return {
104
+ editionDate: parsed.editionDate,
105
+ canonicalPath,
106
+ isCanonical: currentPath === canonicalPath,
107
+ articleSlug: input.articleSlug,
108
+ };
109
+ }
110
+
111
+ export function parseEditionSectionRoute(input: EditionDateRouteInput & { sectionKey: string }): ParsedEditionSectionRoute | null {
112
+ const parsed = parseEditionDateSegments(input);
113
+ if (!parsed) return null;
114
+ if (!input.sectionKey.trim()) return null;
115
+
116
+ const canonicalPath = getEditionSectionPath(parsed.editionDate, input.sectionKey);
117
+ const currentPath = `${getCurrentDatePath(input)}/section/${input.sectionKey}`;
118
+ return {
119
+ editionDate: parsed.editionDate,
120
+ canonicalPath,
121
+ isCanonical: currentPath === canonicalPath,
122
+ sectionKey: input.sectionKey,
123
+ };
124
+ }
125
+
126
+ function parseEditionDateSegments(input: EditionDateRouteInput): Omit<ParsedEditionDateRoute, "isCanonical"> | null {
127
+ if (!/^\d{4}$/.test(input.year)) return null;
128
+
129
+ const monthIndex = MONTH_NAMES.indexOf(input.month.toLowerCase() as (typeof MONTH_NAMES)[number]);
130
+ if (monthIndex < 0) return null;
131
+
132
+ if (!/^\d{1,2}$/.test(input.day)) return null;
133
+ const dayNumber = Number(input.day);
134
+ if (dayNumber < 1 || dayNumber > 31) return null;
135
+
136
+ const monthNumber = monthIndex + 1;
137
+ const canonicalDay = String(dayNumber).padStart(2, "0");
138
+ const editionDate = `${input.year}-${String(monthNumber).padStart(2, "0")}-${canonicalDay}`;
139
+ if (!isValidIsoDate(editionDate)) return null;
140
+
141
+ return {
142
+ editionDate,
143
+ canonicalPath: getEditionDatePath(editionDate),
144
+ };
145
+ }
146
+
147
+ function getCurrentDatePath(input: EditionDateRouteInput): string {
148
+ return `/${input.year}/${input.month}/${input.day}`;
149
+ }
150
+
151
+ function isValidIsoDate(editionDate: string): boolean {
152
+ const [year, month, day] = editionDate.split("-").map(Number);
153
+ const date = new Date(Date.UTC(year, month - 1, day));
154
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
155
+ }