@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.40

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 (147) hide show
  1. package/amplify/auth/redirect-urls.js +54 -0
  2. package/amplify/auth/redirect-urls.ts +66 -0
  3. package/amplify/auth/resource.js +20 -5
  4. package/amplify/auth/resource.ts +27 -5
  5. package/amplify/data/resource.js +28 -70
  6. package/amplify/data/resource.ts +29 -70
  7. package/amplify/data/schema.js +33 -58
  8. package/amplify/data/schema.ts +34 -58
  9. package/amplify/functions/category-action/handler.ts +5 -3
  10. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  11. package/amplify/functions/console-chat-responder/resource.js +0 -7
  12. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  13. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  14. package/amplify/functions/content-actions/handler.py +45 -3
  15. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  16. package/amplify/inbound-email/ses-intake.js +86 -0
  17. package/amplify/inbound-email/ses-intake.ts +110 -0
  18. package/amplify/rebuild-trigger-settings.js +22 -0
  19. package/amplify/rebuild-trigger-settings.ts +42 -0
  20. package/amplify/resolver-waves.js +26 -0
  21. package/amplify/resolver-waves.ts +34 -0
  22. package/amplify/site-backend-config.ts +36 -1
  23. package/amplify/site-backend-names.js +103 -0
  24. package/amplify/site-backend-names.ts +166 -0
  25. package/amplify/site-backend.js +93 -170
  26. package/amplify/site-backend.ts +126 -196
  27. package/amplify/storage/resource.js +3 -0
  28. package/amplify/storage/resource.ts +3 -0
  29. package/amplify/storage-backups/construct.js +31 -0
  30. package/amplify/storage-backups/construct.ts +47 -0
  31. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  32. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
  33. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  34. package/app/api/media/[...path]/route.ts +5 -16
  35. package/app/api/revalidate/route.ts +8 -7
  36. package/app/articles/[slug]/page.tsx +6 -4
  37. package/app/blog-rhythm.css +278 -0
  38. package/app/globals.css +21 -0
  39. package/app/layout.tsx +16 -0
  40. package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
  41. package/app/newsroom/[section]/page.tsx +4 -3
  42. package/app/newsroom/articles/[id]/page.tsx +15 -0
  43. package/app/newsroom/articles/page.tsx +13 -0
  44. package/app/newsroom/layout.tsx +4 -9
  45. package/app/page.tsx +18 -5
  46. package/app/robots.ts +2 -5
  47. package/bin/papyrus-app.mjs +12 -2
  48. package/bin/papyrus-infra.mjs +29 -3
  49. package/components/amplify-client-provider.tsx +4 -22
  50. package/components/archive-shell.tsx +2 -22
  51. package/components/article-video.tsx +247 -3
  52. package/components/blog-page-background.tsx +23 -6
  53. package/components/markus-ir-preview.tsx +300 -0
  54. package/components/news-desk-taxonomy-client.ts +51 -0
  55. package/components/newspaper.tsx +6 -2
  56. package/components/newsroom-access-gate.tsx +67 -0
  57. package/components/newsroom-article-editor.tsx +741 -0
  58. package/components/newsroom-articles-view.tsx +129 -0
  59. package/components/newsroom-client-shell.tsx +12 -0
  60. package/components/newsroom-ops-overview.tsx +28 -11
  61. package/components/newsroom-ops-shell.tsx +3 -1
  62. package/components/newsroom-references-view.tsx +83 -8
  63. package/components/papyrus-console-shell.tsx +4 -2
  64. package/components/pictogram-figure.tsx +9 -2
  65. package/components/presentation-header.tsx +135 -0
  66. package/components/presentation-rhythm-hrule.tsx +17 -0
  67. package/components/reference-cyclotron-status.tsx +109 -0
  68. package/components/reference-relevance-review.tsx +75 -0
  69. package/components/settings-page.tsx +2 -1
  70. package/components/topic-steering-workspace.tsx +54 -64
  71. package/components/use-blog-header-obstacles.ts +81 -0
  72. package/components/use-brand-fonts-ready.ts +28 -0
  73. package/components/use-rhythm-overlay.ts +29 -0
  74. package/infra/amplify-app-shell.js +212 -50
  75. package/infra/amplify-app-shell.ts +240 -53
  76. package/infra/build-specs.js +14 -5
  77. package/infra/build-specs.ts +17 -7
  78. package/infra/github-oidc-provider.js +24 -0
  79. package/infra/github-oidc-provider.ts +30 -0
  80. package/infra/index.js +1 -0
  81. package/infra/site-config.js +167 -10
  82. package/infra/site-config.ts +196 -14
  83. package/lib/amplify-outputs-path.ts +5 -1
  84. package/lib/blog-feature-solver.ts +459 -0
  85. package/lib/blog-rhythm.ts +134 -0
  86. package/lib/brand-fonts-watch.ts +43 -0
  87. package/lib/brand-fonts.ts +112 -0
  88. package/lib/cached-content-repository.ts +12 -0
  89. package/lib/category-repository.ts +42 -93
  90. package/lib/content-actions-demo.ts +190 -0
  91. package/lib/content-repository.ts +3 -0
  92. package/lib/content-source-context.ts +12 -2
  93. package/lib/content-types.ts +4 -0
  94. package/lib/edition-routes.ts +31 -29
  95. package/lib/graphql-content-repository.ts +61 -9
  96. package/lib/media-route-response.ts +45 -0
  97. package/lib/newspaper-layout.ts +3 -2
  98. package/lib/newsroom-articles.ts +325 -0
  99. package/lib/newsroom-base-path.ts +12 -3
  100. package/lib/newsroom-category-drilldown.ts +3 -2
  101. package/lib/newsroom-demo-articles.ts +90 -0
  102. package/lib/newsroom-forum-routes.ts +6 -4
  103. package/lib/newsroom-index-filters.ts +14 -10
  104. package/lib/newsroom-media-backend.ts +142 -0
  105. package/lib/newsroom-media.ts +182 -0
  106. package/lib/newsroom-nav.ts +3 -0
  107. package/lib/oauth-redirect-priority.ts +40 -0
  108. package/lib/papyrus-web-locations.ts +7 -6
  109. package/lib/presentation-footer.ts +3 -2
  110. package/lib/pretext-layout.ts +12 -3
  111. package/lib/reader-base-path.ts +24 -0
  112. package/lib/reader-static-params.ts +13 -0
  113. package/lib/reference-policy.ts +5 -0
  114. package/lib/relevance-decisions.ts +103 -0
  115. package/lib/relevance-status.ts +63 -0
  116. package/lib/renderer.ts +7 -0
  117. package/lib/revalidate-secret.ts +59 -0
  118. package/lib/robots-policy.ts +14 -0
  119. package/lib/root-route-redirect.ts +16 -0
  120. package/lib/semantic-graph.ts +7 -0
  121. package/lib/site-brand.ts +51 -0
  122. package/lib/staging-gated-path.ts +23 -0
  123. package/lib/staging-preview-object.ts +114 -0
  124. package/lib/video-mode.ts +42 -0
  125. package/lib/video-script.ts +77 -0
  126. package/middleware.ts +23 -18
  127. package/package.json +45 -1
  128. package/renderers/pretext/article-page.tsx +39 -3
  129. package/renderers/pretext/presentation-shared.tsx +113 -0
  130. package/renderers/pretext/presentation-shell.tsx +15 -102
  131. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  132. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  133. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  134. package/routes.manifest.json +45 -2
  135. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  136. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  137. package/scripts/videoml/bundle-common.mjs +129 -0
  138. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  139. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  140. package/scripts/videoml/preview/preview-timing.ts +42 -0
  141. package/scripts/videoml/preview.html +77 -0
  142. package/scripts/videoml/standard-entry.tsx +46 -0
  143. package/src/sync.mjs +30 -6
  144. package/src/with-papyrus.mjs +19 -2
  145. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  146. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  147. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -0,0 +1,300 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ parseMarkusDocument,
4
+ type MarkusBlock,
5
+ type MarkusDirective,
6
+ type MarkusInline,
7
+ type MarkusNode,
8
+ } from "../lib/markus-ir";
9
+
10
+ const TOKEN_PATTERN = /PAPYRUSMARKUP\d{5}END/g;
11
+ const WHOLE_TOKEN_PATTERN = /^PAPYRUSMARKUP\d{5}END$/;
12
+
13
+ type PreviewSidecar = {
14
+ images: Record<string, Record<string, unknown>>;
15
+ citations: Record<string, string[]>;
16
+ citationLists: Record<string, unknown>;
17
+ bibliography: string[];
18
+ };
19
+
20
+ type PreviewContext = {
21
+ sidecar: PreviewSidecar;
22
+ citationNumbers: Map<string, number>;
23
+ imageUrls: Record<string, string>;
24
+ };
25
+
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+
30
+ function readSidecar(envelope: Record<string, unknown>): PreviewSidecar {
31
+ const sidecar = envelope.papyrus;
32
+ if (!isRecord(sidecar)) throw new Error("bodyIr.papyrus is missing");
33
+ const { images, citations, citationLists, bibliography } = sidecar;
34
+ if (!isRecord(images) || !isRecord(citations) || !isRecord(citationLists) || !Array.isArray(bibliography)) {
35
+ throw new Error("bodyIr.papyrus must contain images, citations, citationLists and bibliography");
36
+ }
37
+ return {
38
+ images: images as PreviewSidecar["images"],
39
+ citations: citations as PreviewSidecar["citations"],
40
+ citationLists,
41
+ bibliography: bibliography.map(String),
42
+ };
43
+ }
44
+
45
+ function numberCitationsByFirstAppearance(document: unknown, sidecar: PreviewSidecar): Map<string, number> {
46
+ const numbers = new Map<string, number>();
47
+ for (const token of JSON.stringify(document).match(TOKEN_PATTERN) ?? []) {
48
+ for (const key of sidecar.citations[token] ?? []) {
49
+ if (!numbers.has(key)) numbers.set(key, numbers.size + 1);
50
+ }
51
+ }
52
+ return numbers;
53
+ }
54
+
55
+ function imagePlaceholder(token: string, context: PreviewContext): ReactNode {
56
+ const entry = context.sidecar.images[token];
57
+ const src = typeof entry?.src === "string" ? entry.src : "";
58
+ const alt = typeof entry?.alt === "string" ? entry.alt : "";
59
+ const url = context.imageUrls[src];
60
+ if (url) {
61
+ return (
62
+ // eslint-disable-next-line @next/next/no-img-element
63
+ <img
64
+ alt={alt}
65
+ className="my-3 h-auto max-w-full rounded-lg"
66
+ data-markus-preview-image-loaded
67
+ loading="lazy"
68
+ src={url}
69
+ />
70
+ );
71
+ }
72
+ return (
73
+ <div
74
+ className="my-3 rounded-lg border border-dashed border-border bg-muted/30 px-3 py-6 text-center text-xs text-muted-foreground"
75
+ data-markus-preview-image
76
+ >
77
+ {`Image: ${src} (${alt})`}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ function renderText(text: string, context: PreviewContext): ReactNode {
83
+ const parts: ReactNode[] = [];
84
+ let cursor = 0;
85
+ for (const match of text.matchAll(TOKEN_PATTERN)) {
86
+ const token = match[0];
87
+ const index = match.index ?? 0;
88
+ const keys = context.sidecar.citations[token];
89
+ if (!keys) continue;
90
+ if (index > cursor) parts.push(text.slice(cursor, index));
91
+ parts.push(
92
+ <sup className="text-primary" key={`${index}-${token}`}>
93
+ {keys.map((key) => `[${context.citationNumbers.get(key)}]`).join("")}
94
+ </sup>,
95
+ );
96
+ cursor = index + token.length;
97
+ }
98
+ if (cursor < text.length) parts.push(text.slice(cursor));
99
+ return parts;
100
+ }
101
+
102
+ function renderInlines(inlines: MarkusInline[], context: PreviewContext): ReactNode[] {
103
+ return inlines.map((node, index) => {
104
+ switch (node.type) {
105
+ case "text":
106
+ return <span key={index}>{renderText(node.text, context)}</span>;
107
+ case "emphasis":
108
+ return <em key={index}>{renderInlines(node.children, context)}</em>;
109
+ case "strong":
110
+ return <strong key={index}>{renderInlines(node.children, context)}</strong>;
111
+ case "strikethrough":
112
+ return <s key={index}>{renderInlines(node.children, context)}</s>;
113
+ case "code_span":
114
+ return <code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]" key={index}>{node.code}</code>;
115
+ case "link":
116
+ return node.href ? (
117
+ <a className="text-primary underline" href={node.href} key={index} rel="noopener noreferrer" target="_blank">
118
+ {renderInlines(node.children, context)}
119
+ </a>
120
+ ) : (
121
+ <span key={index}>{renderInlines(node.children, context)}</span>
122
+ );
123
+ case "image":
124
+ return (
125
+ <span className="rounded border border-dashed border-border px-1 text-xs text-muted-foreground" key={index}>
126
+ {`Image: ${node.src ?? ""} (${node.alt})`}
127
+ </span>
128
+ );
129
+ case "soft_break":
130
+ return <span key={index}>{" "}</span>;
131
+ case "hard_break":
132
+ return <br key={index} />;
133
+ case "html_inline":
134
+ return <code className="font-mono text-xs" key={index}>{node.value}</code>;
135
+ }
136
+ });
137
+ }
138
+
139
+ function soleToken(inlines: MarkusInline[]): string | null {
140
+ if (inlines.length !== 1 || inlines[0].type !== "text") return null;
141
+ const text = inlines[0].text.trim();
142
+ return WHOLE_TOKEN_PATTERN.test(text) ? text : null;
143
+ }
144
+
145
+ const HEADING_CLASSES = [
146
+ "mt-5 mb-2 text-2xl font-semibold tracking-tight",
147
+ "mt-5 mb-2 text-xl font-semibold tracking-tight",
148
+ "mt-4 mb-2 text-lg font-semibold",
149
+ "mt-4 mb-1 text-base font-semibold",
150
+ "mt-3 mb-1 text-sm font-semibold",
151
+ "mt-3 mb-1 text-sm font-medium",
152
+ ];
153
+
154
+ function renderBlock(block: MarkusBlock, index: number, context: PreviewContext): ReactNode {
155
+ switch (block.type) {
156
+ case "paragraph": {
157
+ const token = soleToken(block.inline);
158
+ if (token && token in context.sidecar.images) return <div key={index}>{imagePlaceholder(token, context)}</div>;
159
+ if (token && token in context.sidecar.citationLists) {
160
+ return (
161
+ <ol className="my-3 list-decimal space-y-1 pl-6 text-sm" data-markus-preview-bibliography key={index}>
162
+ {context.sidecar.bibliography.map((entry, entryIndex) => (
163
+ <li key={entryIndex}>{entry}</li>
164
+ ))}
165
+ </ol>
166
+ );
167
+ }
168
+ return <p className="my-3 leading-relaxed" key={index}>{renderInlines(block.inline, context)}</p>;
169
+ }
170
+ case "heading": {
171
+ const level = Math.min(Math.max(block.level, 1), 6);
172
+ const Tag = `h${level}` as "h1";
173
+ return <Tag className={HEADING_CLASSES[level - 1]} key={index}>{renderInlines(block.inline, context)}</Tag>;
174
+ }
175
+ case "list": {
176
+ const items = block.items.map((item, itemIndex) => (
177
+ <li key={itemIndex}>{renderBlocks(item.children, context)}</li>
178
+ ));
179
+ return block.ordered ? (
180
+ <ol className="my-3 list-decimal space-y-1 pl-6" key={index} start={block.start ?? undefined}>{items}</ol>
181
+ ) : (
182
+ <ul className="my-3 list-disc space-y-1 pl-6" key={index}>{items}</ul>
183
+ );
184
+ }
185
+ case "blockquote":
186
+ return (
187
+ <blockquote className="my-3 border-l-4 border-border pl-4 italic text-muted-foreground" key={index}>
188
+ {renderBlocks(block.children, context)}
189
+ </blockquote>
190
+ );
191
+ case "code":
192
+ return (
193
+ <pre className="my-3 overflow-x-auto rounded-lg bg-muted p-3 font-mono text-xs" key={index}>
194
+ <code>{block.value}</code>
195
+ </pre>
196
+ );
197
+ case "thematic_break":
198
+ return <hr className="my-4 border-border" key={index} />;
199
+ case "table":
200
+ return (
201
+ <div className="my-3 overflow-x-auto" key={index}>
202
+ <table className="w-full border-collapse text-sm">
203
+ <thead>
204
+ <tr>
205
+ {block.header.map((cell, cellIndex) => (
206
+ <th className="border border-border bg-muted/40 px-2 py-1 text-left" key={cellIndex}>
207
+ {renderInlines(cell, context)}
208
+ </th>
209
+ ))}
210
+ </tr>
211
+ </thead>
212
+ <tbody>
213
+ {block.rows.map((row, rowIndex) => (
214
+ <tr key={rowIndex}>
215
+ {row.map((cell, cellIndex) => (
216
+ <td className="border border-border px-2 py-1" key={cellIndex}>{renderInlines(cell, context)}</td>
217
+ ))}
218
+ </tr>
219
+ ))}
220
+ </tbody>
221
+ </table>
222
+ </div>
223
+ );
224
+ case "html":
225
+ return (
226
+ <pre className="my-3 overflow-x-auto rounded-lg bg-muted p-3 font-mono text-xs" key={index}>{block.value}</pre>
227
+ );
228
+ }
229
+ }
230
+
231
+ function renderDirective(directive: MarkusDirective, index: number, context: PreviewContext): ReactNode {
232
+ if (directive.name === "pull-quote") {
233
+ const attribution = directive.attributes.attribution;
234
+ return (
235
+ <blockquote
236
+ className="my-4 border-y border-border px-4 py-3 text-lg font-medium italic"
237
+ data-markus-preview-directive="pull-quote"
238
+ key={index}
239
+ >
240
+ {renderBlocks(directive.children, context)}
241
+ {typeof attribution === "string" && attribution ? (
242
+ <footer className="mt-1 text-sm not-italic text-muted-foreground">{`— ${attribution}`}</footer>
243
+ ) : null}
244
+ </blockquote>
245
+ );
246
+ }
247
+ const attributeEntries = Object.entries(directive.attributes).filter(([, value]) => value !== null && value !== "");
248
+ return (
249
+ <section
250
+ className="my-3 rounded-lg border border-border"
251
+ data-markus-preview-directive={directive.name}
252
+ key={index}
253
+ >
254
+ <header className="border-b border-border bg-muted/40 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
255
+ {directive.name}
256
+ </header>
257
+ <div className="px-3 py-1">
258
+ {directive.leaf && attributeEntries.length > 0 ? (
259
+ <p className="my-2 font-mono text-xs text-muted-foreground">
260
+ {attributeEntries.map(([key, value]) => `${key}=${String(value)}`).join(" ")}
261
+ </p>
262
+ ) : null}
263
+ {renderBlocks(directive.children, context)}
264
+ </div>
265
+ </section>
266
+ );
267
+ }
268
+
269
+ function renderBlocks(nodes: MarkusNode[], context: PreviewContext): ReactNode[] {
270
+ return nodes.map((node, index) =>
271
+ node.type === "directive" ? renderDirective(node, index, context) : renderBlock(node, index, context),
272
+ );
273
+ }
274
+
275
+ export function MarkusIrPreview({
276
+ bodyIr,
277
+ imageUrls = {},
278
+ }: {
279
+ bodyIr: unknown;
280
+ imageUrls?: Record<string, string>;
281
+ }) {
282
+ try {
283
+ const envelope = typeof bodyIr === "string" ? JSON.parse(bodyIr) : bodyIr;
284
+ if (!isRecord(envelope)) throw new Error("bodyIr must be a JSON object");
285
+ const document = parseMarkusDocument(envelope.document);
286
+ const sidecar = readSidecar(envelope);
287
+ const context: PreviewContext = {
288
+ sidecar,
289
+ citationNumbers: numberCitationsByFirstAppearance(envelope.document, sidecar),
290
+ imageUrls,
291
+ };
292
+ return <div data-markus-preview>{renderBlocks(document.children, context)}</div>;
293
+ } catch (error) {
294
+ return (
295
+ <p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive" data-markus-preview-error role="alert">
296
+ {`The preview could not be rendered: ${error instanceof Error ? error.message : "unknown error"}`}
297
+ </p>
298
+ );
299
+ }
300
+ }
@@ -3682,3 +3682,54 @@ function normalizeUnknownErrorMessage(error: unknown, fallback: string): string
3682
3682
  return fallback;
3683
3683
  }
3684
3684
  }
3685
+
3686
+ const CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION = `
3687
+ mutation CreateKnowledgeRawPayload($input: CreateKnowledgeRawPayloadInput!) {
3688
+ createKnowledgeRawPayload(input: $input) { id }
3689
+ }
3690
+ `;
3691
+
3692
+ const GET_KNOWLEDGE_RAW_PAYLOAD_QUERY = `
3693
+ query GetKnowledgeRawPayload($id: ID!) {
3694
+ getKnowledgeRawPayload(id: $id) { id }
3695
+ }
3696
+ `;
3697
+
3698
+ /** The relevance cyclotron's latest cyclotron-status/v1 snapshot, as the sweep recorded it. */
3699
+ export async function loadCyclotronStatus(cyclotronId: string): Promise<import("cyclotron/cyclotron-status").CyclotronStatus | null> {
3700
+ const { cyclotronStatusPayloadId, latestCyclotronStatus } = await import("../lib/relevance-status");
3701
+ const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", cyclotronStatusPayloadId(cyclotronId), ["raw_payload"]);
3702
+ return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3703
+ }
3704
+
3705
+ /** Ask the next decide-relevance sweep to set or clear a manual review rate. */
3706
+ export async function requestCyclotronReviewRate(
3707
+ cyclotronId: string,
3708
+ request: import("../lib/relevance-status").ReviewRateRequest,
3709
+ ): Promise<void> {
3710
+ const { reviewRateRequestPayloadId, safeId } = await import("../lib/relevance-status");
3711
+ const id = reviewRateRequestPayloadId(cyclotronId);
3712
+ const existing = await runGraphql<{ getKnowledgeRawPayload?: { id: string } | null }>(GET_KNOWLEDGE_RAW_PAYLOAD_QUERY, { id });
3713
+ if (!existing.getKnowledgeRawPayload) {
3714
+ await runGraphql(CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION, {
3715
+ input: {
3716
+ id,
3717
+ ownerType: "cyclotron",
3718
+ ownerId: safeId(cyclotronId),
3719
+ payloadKind: "cyclotron-review-rate-request",
3720
+ createdAt: request.requestedAt,
3721
+ updatedAt: request.requestedAt,
3722
+ },
3723
+ });
3724
+ }
3725
+ await uploadModelPayloadForOwner({
3726
+ ownerKind: "knowledgeRawPayload",
3727
+ ownerId: id,
3728
+ ownerLineageId: id,
3729
+ role: "raw_payload",
3730
+ sortKey: `review-rate-request-${request.requestedAt}`,
3731
+ filename: "review-rate-request.json",
3732
+ mediaType: "application/json",
3733
+ content: JSON.stringify(request),
3734
+ });
3735
+ }
@@ -12,6 +12,8 @@ import { ARCHIVE_PREVIEW_HEIGHT, ARCHIVE_PREVIEW_WIDTH } from "../lib/archive-ty
12
12
  import type { EditionContent, NewsDeskAppendix, NewsDeskCategoryTreeNode } from "../lib/content-types";
13
13
  import { shouldBypassImageOptimization } from "../lib/image-url";
14
14
  import type { PublicationItem } from "../lib/publication-items";
15
+ import { clearPretextMeasurementCache } from "../lib/pretext-layout";
16
+ import { toPublicReaderPath } from "../lib/reader-base-path";
15
17
  import { SITE_BRAND } from "../lib/site-brand";
16
18
  import { resolveThemedImageSrc, type ResolvedTheme } from "../lib/themed-image";
17
19
  import { loadEditorCategoryTreeState } from "./news-desk-taxonomy-client";
@@ -130,7 +132,9 @@ export function Newspaper({
130
132
  if (!fonts) return;
131
133
  let cancelled = false;
132
134
  const refreshAfterFontLoad = () => {
133
- if (!cancelled) setFontRevision((revision) => revision + 1);
135
+ if (cancelled) return;
136
+ clearPretextMeasurementCache();
137
+ setFontRevision((revision) => revision + 1);
134
138
  };
135
139
  fonts.ready.then(refreshAfterFontLoad).catch(() => undefined);
136
140
  fonts.addEventListener("loadingdone", refreshAfterFontLoad);
@@ -1877,7 +1881,7 @@ function getPageHref(pageNumber: number, editionBasePath?: string): string {
1877
1881
  }
1878
1882
 
1879
1883
  function getArticleHref(articleSlug: string, editionBasePath?: string): string {
1880
- return editionBasePath ? `${normalizePath(editionBasePath)}/${encodeURIComponent(articleSlug)}` : `/articles/${articleSlug}`;
1884
+ return editionBasePath ? `${normalizePath(editionBasePath)}/${encodeURIComponent(articleSlug)}` : toPublicReaderPath(`/articles/${articleSlug}`);
1881
1885
  }
1882
1886
 
1883
1887
  function getItemAnchorHref(articleSlug: string, pageNumber: number, editionBasePath?: string): string {
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { newsroomHref } from "../lib/newsroom-base-path";
4
+ import Link from "next/link";
5
+ import { SITE_BRAND } from "../lib/site-brand";
6
+ import type { NewsDeskShellState } from "../lib/news-desk-session";
7
+ import { cn } from "../lib/utils";
8
+ import { NewsroomOpsShell } from "./newsroom-ops-shell";
9
+ import { ReaderAuthControl } from "./reader-auth-control";
10
+ import { buttonVariants } from "./ui/button";
11
+
12
+ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
13
+ const accessPhase = shell?.phase ?? "checkingAccess";
14
+
15
+ return (
16
+ <NewsroomOpsShell
17
+ activeTab="overview"
18
+ appTitle={SITE_BRAND.appTitle}
19
+ backHref="/"
20
+ backLabel={SITE_BRAND.backToHomeLabel}
21
+ headerActions={(
22
+ <Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
23
+ )}
24
+ pageTitle="Newsroom"
25
+ showNavigation={showSectionTabs}
26
+ >
27
+ <section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
28
+ <div className="space-y-3 rounded-xl border border-border bg-card p-6">
29
+ <p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
30
+ <h2 className="m-0 font-sans text-xl font-semibold tracking-tight">{formatAccessTitle(shell)}</h2>
31
+ <p className="m-0 text-sm text-muted-foreground">{formatAccessDetail(shell)}</p>
32
+ {shell?.error ? <p className="text-sm text-destructive">{shell.error}</p> : null}
33
+ <p className="m-0 text-sm text-foreground">{formatAccessActionDetail(shell)}</p>
34
+ {shell?.phase === "signedOut" || shell?.phase === "error" ? (
35
+ <div className="news-desk-access-panel__auth">
36
+ <ReaderAuthControl postAuthPath={newsroomHref()} showIdentity />
37
+ </div>
38
+ ) : null}
39
+ </div>
40
+ </section>
41
+ </NewsroomOpsShell>
42
+ );
43
+ }
44
+
45
+ function formatAccessTitle(state: NewsDeskShellState | null): string {
46
+ if (!state || state.phase === "checkingAccess") return "Checking Desk Credentials";
47
+ if (state.phase === "loadingDesk") return "Loading Newsroom Records";
48
+ if (state.phase === "forbidden") return "Editor Role Required";
49
+ if (state.phase === "error") return "Newsroom Unavailable";
50
+ return "Editor Sign-In Required";
51
+ }
52
+
53
+ function formatAccessDetail(state: NewsDeskShellState | null): string {
54
+ if (!state || state.phase === "checkingAccess") return `${SITE_BRAND.appTitle} is checking the current browser session before loading steering state.`;
55
+ if (state.phase === "loadingDesk") return `${SITE_BRAND.appTitle} verified the browser session and is loading private Newsroom records.`;
56
+ if (state.phase === "forbidden") return "This account is signed in, but the Cognito session does not include the editor or admin group.";
57
+ if (state.phase === "error") return `${SITE_BRAND.appTitle} could not verify this editor session or load the private Newsroom data.`;
58
+ return "Sign in with an editor or admin account to inspect category, category tree, ontology, and graph steering.";
59
+ }
60
+
61
+ function formatAccessActionDetail(state: NewsDeskShellState | null): string {
62
+ if (!state || state.phase === "checkingAccess") return "This should only take a moment. If it hangs, reload the page.";
63
+ if (state.phase === "loadingDesk") return "Private records are loading from GraphQL. Large reference queues may take several seconds.";
64
+ if (state.phase === "forbidden") return "Ask an admin to add this account to the editor or admin group, then sign out and back in.";
65
+ if (state.phase === "error") return "Reload the page or check the local development console for the GraphQL error.";
66
+ return "Use the login button above to authenticate with an editor or admin account.";
67
+ }