@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,74 @@
1
+ import type { HydratedModelPayload } from "./category-repository";
2
+ import type { ReferenceRecord } from "./category-repository";
3
+ import type { SemanticGraphSnapshot } from "./semantic-graph";
4
+
5
+ function metadataRecord(value: unknown): Record<string, unknown> | null {
6
+ if (typeof value === "string") {
7
+ try {
8
+ return metadataRecord(JSON.parse(value));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
14
+ return value as Record<string, unknown>;
15
+ }
16
+
17
+ function normalizeDisplayText(value: unknown): string | null {
18
+ if (typeof value !== "string") return null;
19
+ const trimmed = value.trim();
20
+ return trimmed || null;
21
+ }
22
+
23
+ export function referenceMetadataField(
24
+ payload: HydratedModelPayload | null,
25
+ fallback: unknown,
26
+ key: "title" | "subtitle" | "summary",
27
+ ): string | null {
28
+ const payloadRecord = metadataRecord(payload?.json);
29
+ if (payloadRecord) {
30
+ const value = normalizeDisplayText(payloadRecord[key]);
31
+ if (value) return value;
32
+ }
33
+ const fallbackRecord = metadataRecord(fallback);
34
+ return fallbackRecord ? normalizeDisplayText(fallbackRecord[key]) : null;
35
+ }
36
+
37
+ export function referenceSummaryFromGraph(graph: SemanticGraphSnapshot | null | undefined, lineageId: string): string | null {
38
+ if (!graph || !lineageId) return null;
39
+ for (const message of graph.summariesFor("reference", lineageId)) {
40
+ const summary = normalizeDisplayText(message.summary);
41
+ if (!summary) continue;
42
+ if (/can['’]t summarize|don['’]t have the document|link alone/i.test(summary)) continue;
43
+ return summary;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function referenceDisplaySummary(
49
+ graph: SemanticGraphSnapshot | null | undefined,
50
+ lineageId: string,
51
+ payload: HydratedModelPayload | null,
52
+ fallback?: unknown,
53
+ ): string | null {
54
+ return referenceMetadataField(payload, fallback, "summary")
55
+ ?? referenceSummaryFromGraph(graph, lineageId);
56
+ }
57
+
58
+ export function resolveCanonicalReferenceLineage(
59
+ references: ReferenceRecord[],
60
+ requestedLineageId: string,
61
+ ): string {
62
+ if (!requestedLineageId) return requestedLineageId;
63
+ const target = references.find((reference) => (reference.lineageId ?? reference.id) === requestedLineageId);
64
+ if (!target?.sourceUri) return requestedLineageId;
65
+ const duplicates = references.filter((reference) =>
66
+ reference.versionState === "current"
67
+ && reference.sourceUri === target.sourceUri);
68
+ if (duplicates.length <= 1) return requestedLineageId;
69
+ const preferred = duplicates.find((reference) => reference.curationStatus !== "archived")
70
+ ?? duplicates.find((reference) => !(reference.lineageId ?? "").includes("research-proposal"))
71
+ ?? target;
72
+ return preferred.lineageId ?? preferred.id ?? requestedLineageId;
73
+ }
74
+
@@ -0,0 +1,97 @@
1
+ import type { MessageRecord, ReferenceRecord } from "./category-repository";
2
+
3
+ export const REFERENCE_CURATION_STATUSES = ["pending", "accepted", "rejected", "archived"] as const;
4
+ export type ReferenceCurationStatus = typeof REFERENCE_CURATION_STATUSES[number];
5
+
6
+ export const REFERENCE_REJECTION_REASON_CODES = [
7
+ "out_of_scope",
8
+ "policy_exclusion",
9
+ "duplicate",
10
+ "low_quality",
11
+ "unavailable",
12
+ "provenance",
13
+ "other",
14
+ ] as const;
15
+ export type ReferenceRejectionReasonCode = typeof REFERENCE_REJECTION_REASON_CODES[number];
16
+
17
+ const REFERENCE_REJECTION_REASON_CODE_SET = new Set<string>(REFERENCE_REJECTION_REASON_CODES);
18
+ const SCOPE_TRAINING_NEGATIVE_REASON_CODE_SET = new Set<string>(["out_of_scope", "policy_exclusion"]);
19
+
20
+ export function normalizeReferenceCurationStatus(value: unknown, defaultStatus: ReferenceCurationStatus = "pending"): ReferenceCurationStatus {
21
+ const normalized = normalizePolicyToken(value ?? defaultStatus);
22
+ if (normalized === "accepted" || normalized === "accept" || normalized === "ready" || normalized === "trusted") return "accepted";
23
+ if (normalized === "rejected" || normalized === "reject" || normalized === "discarded") return "rejected";
24
+ if (normalized === "archived" || normalized === "archive") return "archived";
25
+ return "pending";
26
+ }
27
+
28
+ export function referenceCurationStatusForAction(action: "accept" | "reject" | "reopen" | "archive"): ReferenceCurationStatus {
29
+ if (action === "accept") return "accepted";
30
+ if (action === "reject") return "rejected";
31
+ if (action === "archive") return "archived";
32
+ return "pending";
33
+ }
34
+
35
+ export function normalizeReferenceRejectionReasonCode(value: unknown): ReferenceRejectionReasonCode | null {
36
+ const normalized = normalizePolicyToken(value);
37
+ if (!normalized) return null;
38
+ return REFERENCE_REJECTION_REASON_CODE_SET.has(normalized) ? normalized as ReferenceRejectionReasonCode : null;
39
+ }
40
+
41
+ export function isEvidenceEligibleReference(reference: Pick<ReferenceRecord, "versionState" | "curationStatus"> | null | undefined): boolean {
42
+ return Boolean(reference)
43
+ && reference?.versionState === "current"
44
+ && normalizeReferenceCurationStatus(reference?.curationStatus) === "accepted";
45
+ }
46
+
47
+ export function referenceReasonCode(reference: Pick<ReferenceRecord, "metadata"> | null | undefined, messages: Array<Pick<MessageRecord, "metadata" | "createdAt">> = []): ReferenceRejectionReasonCode | null {
48
+ const metadataReason = reasonCodeFromMetadata(reference?.metadata);
49
+ if (metadataReason) return metadataReason;
50
+ const sortedMessages = [...messages].sort((left, right) => String(right.createdAt ?? "").localeCompare(String(left.createdAt ?? "")));
51
+ for (const message of sortedMessages) {
52
+ const reason = reasonCodeFromMetadata(message.metadata);
53
+ if (reason) return reason;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ export function scopeTrainingLabelForReference(reference: Pick<ReferenceRecord, "versionState" | "curationStatus" | "metadata"> | null | undefined, messages: Array<Pick<MessageRecord, "metadata" | "createdAt">> = []): "positive" | "negative" | null {
59
+ if (!reference || reference.versionState !== "current") return null;
60
+ const status = normalizeReferenceCurationStatus(reference.curationStatus);
61
+ if (status === "accepted") return "positive";
62
+ if (status !== "rejected") return null;
63
+ const reasonCode = referenceReasonCode(reference, messages);
64
+ return reasonCode && SCOPE_TRAINING_NEGATIVE_REASON_CODE_SET.has(reasonCode) ? "negative" : null;
65
+ }
66
+
67
+ function reasonCodeFromMetadata(metadata: unknown): ReferenceRejectionReasonCode | null {
68
+ const parsed = parseMetadataObject(metadata);
69
+ return normalizeReferenceRejectionReasonCode(
70
+ parsed.reasonCode
71
+ ?? parsed.reason_code
72
+ ?? parsed.curationReasonCode
73
+ ?? parsed.curation_reason_code
74
+ ?? parsed.rejectionReasonCode
75
+ ?? parsed.rejection_reason_code,
76
+ );
77
+ }
78
+
79
+ function parseMetadataObject(value: unknown): Record<string, unknown> {
80
+ if (!value) return {};
81
+ if (typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
82
+ if (typeof value !== "string") return {};
83
+ try {
84
+ const parsed = JSON.parse(value);
85
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
86
+ } catch {
87
+ return {};
88
+ }
89
+ }
90
+
91
+ function normalizePolicyToken(value: unknown): string {
92
+ return String(value ?? "")
93
+ .trim()
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9]+/g, "_")
96
+ .replace(/^_|_$/g, "");
97
+ }
@@ -0,0 +1,175 @@
1
+ import type { ReferenceAttachmentRecord } from "./category-repository";
2
+
3
+ export type ReferenceSourcePreview =
4
+ | {
5
+ kind: "youtube";
6
+ videoId: string;
7
+ embedUrl: string;
8
+ thumbnailUrl: string;
9
+ watchUrl: string;
10
+ }
11
+ | {
12
+ kind: "pdf";
13
+ href: string;
14
+ label: string;
15
+ embedUrl: string;
16
+ }
17
+ | {
18
+ kind: "html";
19
+ href: string;
20
+ label: string;
21
+ };
22
+
23
+ export function youtubeVideoIdFromUri(sourceUri: string): string | null {
24
+ try {
25
+ const parsed = new URL(sourceUri);
26
+ const host = parsed.hostname.toLowerCase();
27
+ if (host === "youtu.be") {
28
+ const token = parsed.pathname.replace(/^\/+/, "").split("/")[0] ?? "";
29
+ return token || null;
30
+ }
31
+ if (host === "youtube.com" || host === "www.youtube.com" || host === "m.youtube.com") {
32
+ const fromQuery = parsed.searchParams.get("v");
33
+ if (fromQuery) return fromQuery;
34
+ if (parsed.pathname.startsWith("/shorts/")) {
35
+ return parsed.pathname.slice("/shorts/".length).split("/")[0] || null;
36
+ }
37
+ }
38
+ } catch {
39
+ return null;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export function isPdfAttachment(attachment: ReferenceAttachmentRecord): boolean {
45
+ const mediaType = (attachment.mediaType ?? "").toLowerCase();
46
+ if (mediaType.includes("pdf")) return true;
47
+ const path = `${attachment.storagePath ?? ""} ${attachment.sourceUri ?? ""} ${attachment.filename ?? ""}`.toLowerCase();
48
+ return path.includes(".pdf");
49
+ }
50
+
51
+ function isHtmlAttachment(attachment: ReferenceAttachmentRecord): boolean {
52
+ const mediaType = (attachment.mediaType ?? "").toLowerCase();
53
+ if (mediaType.includes("html")) return true;
54
+ const path = `${attachment.storagePath ?? ""} ${attachment.sourceUri ?? ""} ${attachment.filename ?? ""}`.toLowerCase();
55
+ return path.endsWith(".html") || path.endsWith(".htm");
56
+ }
57
+
58
+ export function isCitationLandingPageUrl(uri: string): boolean {
59
+ try {
60
+ const parsed = new URL(uri);
61
+ const host = parsed.hostname.toLowerCase();
62
+ const path = parsed.pathname.toLowerCase();
63
+ if (host.endsWith("arxiv.org") && path.startsWith("/abs/")) return true;
64
+ if (host === "doi.org" || host.endsWith(".doi.org")) return true;
65
+ if (host.includes("pubmed") && path.includes("/")) return true;
66
+ return false;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ export function isDirectPdfUrl(uri: string): boolean {
73
+ try {
74
+ const parsed = new URL(uri);
75
+ if (parsed.pathname.toLowerCase().endsWith(".pdf")) return true;
76
+ if (parsed.searchParams.get("response-content-type")?.toLowerCase().includes("pdf")) return true;
77
+ } catch {
78
+ return uri.toLowerCase().includes(".pdf");
79
+ }
80
+ return uri.toLowerCase().includes(".pdf");
81
+ }
82
+
83
+ export function pickPdfAttachmentHref(
84
+ attachment: ReferenceAttachmentRecord,
85
+ referenceSourceUri?: string | null,
86
+ ): string | null {
87
+ const candidates = [attachment.sourceUri, referenceSourceUri].filter(
88
+ (value): value is string => Boolean(value?.trim()),
89
+ );
90
+
91
+ for (const candidate of candidates) {
92
+ if (isDirectPdfUrl(candidate)) return candidate;
93
+ }
94
+
95
+ for (const candidate of candidates) {
96
+ if (!isCitationLandingPageUrl(candidate)) return candidate;
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ function pdfPreviewFromUri(sourceUri: string | null | undefined): ReferenceSourcePreview | null {
103
+ if (!sourceUri) return null;
104
+ if (!isDirectPdfUrl(sourceUri)) return null;
105
+ return {
106
+ kind: "pdf",
107
+ href: sourceUri,
108
+ embedUrl: sourceUri,
109
+ label: "Open PDF source",
110
+ };
111
+ }
112
+
113
+ function htmlPreviewFromUri(sourceUri: string | null | undefined): ReferenceSourcePreview | null {
114
+ if (!sourceUri) return null;
115
+ try {
116
+ const parsed = new URL(sourceUri);
117
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
118
+ if (youtubeVideoIdFromUri(sourceUri)) return null;
119
+ const lowerPath = parsed.pathname.toLowerCase();
120
+ if (lowerPath.endsWith(".pdf")) return null;
121
+ return { kind: "html", href: sourceUri, label: "Open web source" };
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ export function resolveReferenceSourcePreview(
128
+ sourceUri: string | null | undefined,
129
+ attachments: ReferenceAttachmentRecord[] = [],
130
+ ): ReferenceSourcePreview | null {
131
+ const videoId = sourceUri ? youtubeVideoIdFromUri(sourceUri) : null;
132
+ if (videoId) {
133
+ const watchUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
134
+ return {
135
+ kind: "youtube",
136
+ videoId,
137
+ embedUrl: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}`,
138
+ thumbnailUrl: `https://i.ytimg.com/vi/${encodeURIComponent(videoId)}/hqdefault.jpg`,
139
+ watchUrl,
140
+ };
141
+ }
142
+
143
+ const pdfAttachment = attachments.find((attachment) => isPdfAttachment(attachment));
144
+ if (pdfAttachment) {
145
+ const href = pickPdfAttachmentHref(pdfAttachment, sourceUri);
146
+ if (href) {
147
+ return {
148
+ kind: "pdf",
149
+ href,
150
+ embedUrl: href,
151
+ label: pdfAttachment.filename ?? "Open PDF attachment",
152
+ };
153
+ }
154
+ }
155
+
156
+ const pdfFromUri = pdfPreviewFromUri(sourceUri);
157
+ if (pdfFromUri) return pdfFromUri;
158
+
159
+ const htmlAttachment = attachments.find((attachment) => isHtmlAttachment(attachment));
160
+ if (htmlAttachment) {
161
+ const candidates = [htmlAttachment.sourceUri, htmlAttachment.storagePath, sourceUri].filter(
162
+ (value): value is string => Boolean(value?.trim()),
163
+ );
164
+ const href = candidates.find((candidate) => !isCitationLandingPageUrl(candidate)) ?? candidates[0];
165
+ if (href) {
166
+ return {
167
+ kind: "html",
168
+ href,
169
+ label: htmlAttachment.filename ?? "Open HTML attachment",
170
+ };
171
+ }
172
+ }
173
+
174
+ return htmlPreviewFromUri(sourceUri);
175
+ }
@@ -0,0 +1,50 @@
1
+ import type { EditionLayoutPlan } from "./layout-plan";
2
+
3
+ export type PretextLayout = "newsprint" | "blog" | "magazine";
4
+
5
+ export type PretextRendererConfig = {
6
+ kind: "pretext";
7
+ layout: PretextLayout;
8
+ layoutPlan: EditionLayoutPlan;
9
+ };
10
+
11
+ export type MarkusRendererConfig = {
12
+ kind: "markus";
13
+ theme: string;
14
+ };
15
+
16
+ export type RendererConfig = PretextRendererConfig | MarkusRendererConfig;
17
+
18
+ /** Sibling deploy target — type-only in PPY-eca592; not a required SiteBrand field. */
19
+ export type HostingConfig =
20
+ | { kind: "amplify-ssr" }
21
+ | { kind: "amplify-static" };
22
+
23
+ export function isPretextRendererConfig(config: RendererConfig): config is PretextRendererConfig {
24
+ return config.kind === "pretext";
25
+ }
26
+
27
+ export function isMarkusRendererConfig(config: RendererConfig): config is MarkusRendererConfig {
28
+ return config.kind === "markus";
29
+ }
30
+
31
+ export function assertPretextRendererConfig(config: RendererConfig): PretextRendererConfig {
32
+ if (config.kind !== "pretext") {
33
+ throw new Error(
34
+ `Site renderer is "${config.kind}"; Next.js reader routes require kind "pretext". `
35
+ + "Markus sites use `poetry run papyrus renderers markus-build` and static hosting.",
36
+ );
37
+ }
38
+ return config;
39
+ }
40
+
41
+ export class MarkusSiteRendererError extends Error {
42
+ constructor(theme: string, siteBrandId = "unknown") {
43
+ super(
44
+ `Site brand "${siteBrandId}" uses Markus renderer (theme "${theme}"). `
45
+ + "Next.js reader routes are not available. "
46
+ + "Build with `poetry run papyrus renderers markus-build` and serve `web/dist/` statically.",
47
+ );
48
+ this.name = "MarkusSiteRendererError";
49
+ }
50
+ }
@@ -0,0 +1,87 @@
1
+ import type { ComponentType } from "react";
2
+ import type { Article } from "./articles";
3
+ import type { EditionContent } from "./content-types";
4
+ import type { PretextLayout } from "./renderer-config";
5
+ import type { PresentationFooterEntry } from "./presentation-footer";
6
+ import type { PublicationItem } from "./publication-items";
7
+
8
+ /**
9
+ * Renderer is the swappable render pipeline that turns standard content
10
+ * (`EditionContent` / `PublicationItem` / `Article`) into a site: today
11
+ * `"pretext"` (the existing client-side layout solver), with `"markus"`
12
+ * (Anthus-flavored Markdown -> static HTML) as the second implementation
13
+ * this shape is designed to admit. See docs/pluggable-publishers.md.
14
+ *
15
+ * `Layout` (`"newsprint" | "blog" | "magazine"`, `PretextLayout`
16
+ * below) is a *Pretext-internal* concern -- it is the look within Pretext,
17
+ * not a concept every renderer needs to understand. A renderer that isn't
18
+ * Pretext may support none of these layouts; `supportsLayout` is how a
19
+ * caller finds out.
20
+ */
21
+ export type RendererId = "pretext" | "markus";
22
+
23
+ export type PresentationTarget =
24
+ | { kind: "edition" }
25
+ | { kind: "section"; sectionKey: string };
26
+
27
+ export type RenderEditionProps = {
28
+ content: EditionContent;
29
+ editionBasePath?: string;
30
+ mastheadHomeHref?: string;
31
+ initialPageNumber?: number;
32
+ lockedPresentation?: PretextLayout;
33
+ target?: PresentationTarget;
34
+ };
35
+
36
+ export type ArticlePageEditionFooter = {
37
+ editionBasePath: string;
38
+ entries: PresentationFooterEntry[];
39
+ subtitle: string;
40
+ title?: string;
41
+ };
42
+
43
+ export type RenderArticleProps = {
44
+ article: Article;
45
+ backHref: string;
46
+ backLabel?: string;
47
+ editionFooter?: ArticlePageEditionFooter;
48
+ editionDate?: string;
49
+ };
50
+
51
+ export type RenderItemProps = {
52
+ item: PublicationItem;
53
+ backHref: string;
54
+ backLabel?: string;
55
+ editionFooter?: ArticlePageEditionFooter;
56
+ editionDate?: string;
57
+ };
58
+
59
+ /**
60
+ * The operations below are derived directly from real call sites, not
61
+ * guessed:
62
+ *
63
+ * - `renderEdition` backs `PresentationShell`, used by the date-scoped
64
+ * edition route (`app/[year]/[month]/[day]/edition-route-page.tsx`).
65
+ * - `renderArticle` / `renderItem` back `ArticlePageView` / `ItemPageView`,
66
+ * used directly by `app/articles/[slug]/page.tsx` and
67
+ * `app/[year]/[month]/[day]/[articleSlug]/page.tsx`.
68
+ * - `stylesheets` lets a renderer declare CSS it owns. Pretext's CSS is not
69
+ * currently extracted from the monolithic `app/globals.css` bundle (it is
70
+ * imported unconditionally by `app/layout.tsx` regardless of renderer), so
71
+ * its implementation returns an empty array today -- this hook exists so a
72
+ * future static-output renderer (Markus's vendored + per-site CSS layers,
73
+ * see docs/pluggable-publishers.md section 6) has somewhere real to report
74
+ * the stylesheets it needs mounted.
75
+ * - `supportsLayout` is the capability/validation hook: since Layout is
76
+ * Pretext-internal, a renderer must be able to say which layouts (if any)
77
+ * it understands, rather than every caller assuming all renderers share
78
+ * Pretext's three layouts.
79
+ */
80
+ export type Renderer = {
81
+ id: RendererId;
82
+ renderEdition: ComponentType<RenderEditionProps>;
83
+ renderArticle: ComponentType<RenderArticleProps>;
84
+ renderItem: ComponentType<RenderItemProps>;
85
+ stylesheets(): string[];
86
+ supportsLayout(layout: PretextLayout): boolean;
87
+ };