@anthusai/papyrus 2.0.0-next.9 → 2.0.0-next.90

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 (217) hide show
  1. package/amplify/auth/resource.js +16 -1
  2. package/amplify/auth/resource.ts +19 -1
  3. package/amplify/data/function-handler.js +20 -0
  4. package/amplify/data/function-handler.ts +40 -0
  5. package/amplify/data/resource.js +151 -61
  6. package/amplify/data/resource.ts +157 -61
  7. package/amplify/data/schema.js +2096 -1980
  8. package/amplify/data/schema.ts +198 -69
  9. package/amplify/functions/assignment-action/data-access.js +2 -0
  10. package/amplify/functions/assignment-action/data-access.ts +2 -0
  11. package/amplify/functions/assignment-action/handler.ts +1 -1
  12. package/amplify/functions/automation-view/handler.py +17 -0
  13. package/amplify/functions/automation-view/resource.js +24 -0
  14. package/amplify/functions/automation-view/resource.ts +34 -0
  15. package/amplify/functions/automation-view/schema.js +26 -0
  16. package/amplify/functions/automation-view/schema.ts +28 -0
  17. package/amplify/functions/automation-view/wire.js +56 -0
  18. package/amplify/functions/automation-view/wire.ts +76 -0
  19. package/amplify/functions/content-actions/handler.py +61 -2
  20. package/amplify/functions/content-actions/requirements.txt +1 -0
  21. package/amplify/functions/content-actions/resource.js +1 -1
  22. package/amplify/functions/content-actions/resource.ts +1 -1
  23. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
  24. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  25. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
  26. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
  27. package/amplify/functions/editorial-checks/schema.js +122 -0
  28. package/amplify/{plugins → functions}/editorial-checks/schema.ts +40 -12
  29. package/amplify/functions/editorial-checks/wire.js +53 -0
  30. package/amplify/functions/editorial-checks/wire.ts +61 -0
  31. package/amplify/functions/monitoring-view/handler.py +11 -0
  32. package/amplify/functions/monitoring-view/resource.js +24 -0
  33. package/amplify/functions/monitoring-view/resource.ts +34 -0
  34. package/amplify/functions/monitoring-view/schema.js +17 -0
  35. package/amplify/functions/monitoring-view/schema.ts +19 -0
  36. package/amplify/functions/monitoring-view/wire.js +37 -0
  37. package/amplify/functions/monitoring-view/wire.ts +47 -0
  38. package/amplify/functions/public-forms/core.js +247 -0
  39. package/amplify/functions/public-forms/core.ts +310 -0
  40. package/amplify/functions/public-forms/handler.ts +41 -0
  41. package/amplify/functions/public-forms/origins.js +35 -0
  42. package/amplify/functions/public-forms/origins.ts +43 -0
  43. package/amplify/functions/public-forms/resource.js +9 -0
  44. package/amplify/functions/public-forms/resource.ts +10 -0
  45. package/amplify/functions/public-forms/wire.js +25 -0
  46. package/amplify/functions/public-forms/wire.ts +32 -0
  47. package/amplify/functions/shared/editorial-context.ts +63 -4
  48. package/amplify/functions/shared/python-bundle.js +20 -14
  49. package/amplify/functions/shared/python-bundle.ts +25 -18
  50. package/amplify/plugins/newsletter/config.js +245 -0
  51. package/amplify/plugins/newsletter/config.ts +305 -0
  52. package/amplify/plugins/newsletter/function/handler.py +6 -0
  53. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  54. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  55. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  56. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  57. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  58. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  59. package/amplify/plugins/newsletter/schema.js +57 -0
  60. package/amplify/plugins/newsletter/schema.ts +62 -0
  61. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  62. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  63. package/amplify/plugins/newsletter/sending.js +140 -0
  64. package/amplify/plugins/newsletter/sending.ts +186 -0
  65. package/amplify/plugins/reader-steering/config.js +182 -0
  66. package/amplify/plugins/reader-steering/config.ts +290 -0
  67. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  68. package/amplify/plugins/reader-steering/function/handler.py +1567 -0
  69. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  70. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  71. package/amplify/plugins/reader-steering/function/proposals.py +458 -0
  72. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  73. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  74. package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
  75. package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
  76. package/amplify/plugins/reader-steering/schema.js +229 -0
  77. package/amplify/plugins/reader-steering/schema.ts +231 -0
  78. package/amplify/plugins/registry.js +4 -2
  79. package/amplify/plugins/registry.ts +4 -2
  80. package/amplify/plugins/signup/schema.js +2 -1
  81. package/amplify/plugins/signup/schema.ts +2 -1
  82. package/amplify/plugins/types.ts +5 -0
  83. package/amplify/plugins/wire.js +1 -0
  84. package/amplify/plugins/wire.ts +1 -0
  85. package/amplify/publication/projection-lease.js +30 -0
  86. package/amplify/publication/projection-lease.ts +39 -0
  87. package/amplify/publication/redirect-and-index.js +105 -0
  88. package/amplify/publication/redirect-rules.js +73 -0
  89. package/amplify/publication/redirect-rules.ts +93 -0
  90. package/amplify/publication/s3-projection.js +7 -1
  91. package/amplify/publication/s3-projection.ts +9 -1
  92. package/amplify/site-backend-config.ts +18 -1
  93. package/amplify/site-backend-names.js +4 -2
  94. package/amplify/site-backend-names.ts +4 -2
  95. package/amplify/site-backend.js +45 -3
  96. package/amplify/site-backend.ts +51 -3
  97. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  98. package/app/api/newsletter/confirm/route.ts +13 -0
  99. package/app/api/newsletter/preferences/route.ts +13 -0
  100. package/app/api/newsletter/state/route.ts +9 -0
  101. package/app/api/newsletter/subscribe/route.ts +9 -0
  102. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  103. package/app/api/newsroom/curation/context/route.ts +9 -0
  104. package/app/api/newsroom/curation/priming/route.ts +9 -0
  105. package/app/api/newsroom/curation/publication/route.ts +9 -0
  106. package/app/api/reader/comment/delete/route.ts +9 -0
  107. package/app/api/reader/comment/edit/route.ts +9 -0
  108. package/app/api/reader/comment/route.ts +9 -0
  109. package/app/api/reader/flag/route.ts +9 -0
  110. package/app/api/reader/handle/route.ts +9 -0
  111. package/app/api/reader/me/route.ts +9 -0
  112. package/app/api/reader/rating/route.ts +9 -0
  113. package/app/api/reader/report/route.ts +9 -0
  114. package/app/api/reader/retired/route.ts +9 -0
  115. package/app/api/reader/submit/route.ts +9 -0
  116. package/app/api/reader/vote/route.ts +9 -0
  117. package/bin/papyrus-infra.mjs +20 -1
  118. package/components/automation-panel.tsx +49 -0
  119. package/components/automation-run-now.tsx +97 -0
  120. package/components/automation-view.tsx +195 -0
  121. package/components/editorial-checks-admin-panel.tsx +38 -13
  122. package/components/moderation-panel.tsx +248 -0
  123. package/components/monitoring-overview-view.tsx +241 -0
  124. package/components/monitoring-panel.tsx +40 -0
  125. package/components/news-desk-client-provider.tsx +18 -18
  126. package/components/news-desk-taxonomy-client.ts +28 -8
  127. package/components/newsletter-panel.tsx +474 -0
  128. package/components/newsletter-sections.tsx +163 -0
  129. package/components/newsroom-access-gate.tsx +16 -2
  130. package/components/newsroom-article-editor.tsx +11 -11
  131. package/components/newsroom-articles-view.tsx +6 -6
  132. package/components/newsroom-assignment-detail.tsx +6 -6
  133. package/components/newsroom-board-view.tsx +8 -3
  134. package/components/newsroom-curation-bootstrap.tsx +45 -0
  135. package/components/newsroom-ops-overview.tsx +26 -96
  136. package/components/newsroom-ops-shell.tsx +2 -2
  137. package/components/newsroom-organizer.css +33 -0
  138. package/components/newsroom-overview-skeleton.tsx +25 -0
  139. package/components/newsroom-references-view.tsx +192 -49
  140. package/components/notification-center.tsx +277 -0
  141. package/components/publication-cyclotron-panel.tsx +15 -10
  142. package/components/reader-auth-state.ts +1 -1
  143. package/components/reference-add-entry.tsx +75 -0
  144. package/components/reference-curation-controls.tsx +31 -22
  145. package/components/reference-cyclotron-priming.tsx +58 -19
  146. package/components/reference-cyclotron-suggestion.tsx +10 -6
  147. package/components/reference-priming.tsx +14 -10
  148. package/components/reference-relevance-review.tsx +3 -3
  149. package/components/reference-section-suggestion.tsx +1 -1
  150. package/components/topic-steering-workspace.tsx +237 -158
  151. package/components/use-editorial-checks.ts +12 -10
  152. package/infra/amplify-app-shell.js +94 -16
  153. package/infra/amplify-app-shell.ts +106 -18
  154. package/infra/cloud-email-config.js +32 -0
  155. package/infra/cloud-email-config.ts +39 -0
  156. package/infra/cloud-email.js +57 -0
  157. package/infra/cloud-email.test.js +69 -0
  158. package/infra/cloud-email.test.ts +66 -0
  159. package/infra/cloud-email.ts +60 -0
  160. package/infra/index.js +1 -0
  161. package/infra/secure-string-parameter-handler.js +41 -0
  162. package/infra/secure-string-parameter-handler.ts +41 -0
  163. package/infra/secure-string-parameter.js +79 -0
  164. package/infra/secure-string-parameter.ts +91 -0
  165. package/infra/site-config.js +88 -6
  166. package/infra/site-config.ts +134 -6
  167. package/lib/automation-client.ts +54 -0
  168. package/lib/automation-fixture.ts +93 -0
  169. package/lib/automation-overview.ts +252 -0
  170. package/lib/category-repository.ts +14 -12
  171. package/lib/console-web-context.ts +1 -1
  172. package/lib/curation-routes.ts +345 -0
  173. package/lib/cyclotron-suggestions.ts +4 -4
  174. package/lib/editorial-checks-backend.ts +1 -1
  175. package/lib/editorial-checks-client.ts +19 -1
  176. package/lib/editorial-checks-demo.ts +2 -2
  177. package/lib/editorial-checks-settings.ts +3 -0
  178. package/lib/editorial-checks.ts +8 -11
  179. package/lib/editorial-preview.ts +5 -5
  180. package/lib/monitoring-client.ts +29 -0
  181. package/lib/monitoring-overview-fixture.ts +51 -0
  182. package/lib/monitoring-overview.ts +145 -0
  183. package/lib/news-desk-session.ts +12 -21
  184. package/lib/newsletter-api.ts +418 -0
  185. package/lib/newsletter-client.ts +46 -0
  186. package/lib/newsletter-fixture.ts +184 -0
  187. package/lib/newsletter-model.ts +202 -0
  188. package/lib/newsroom-demo-dashboard.ts +1 -1
  189. package/lib/newsroom-nav.ts +1 -1
  190. package/lib/newsroom-organizer-nav.ts +2 -1
  191. package/lib/prospectus-session.ts +15 -14
  192. package/lib/prospectus-sign-in-routes.ts +105 -20
  193. package/lib/prospectus-site-identity.ts +46 -6
  194. package/lib/reader-api.ts +145 -0
  195. package/lib/reader-moderation.ts +220 -0
  196. package/lib/reader-retirement-route.ts +89 -0
  197. package/lib/reader-subject.md +29 -0
  198. package/lib/reader-subject.ts +53 -0
  199. package/lib/reference-review-details.ts +71 -0
  200. package/lib/reference-review-request.ts +14 -0
  201. package/lib/relevance-decisions.ts +42 -10
  202. package/lib/relevance-status.ts +2 -2
  203. package/lib/review-client.ts +117 -0
  204. package/lib/review-fixture.ts +45 -0
  205. package/lib/review-requests.ts +244 -0
  206. package/lib/semantic-graph.ts +2 -0
  207. package/lib/site-brand-tenant.ts +11 -6
  208. package/lib/site-brand.ts +6 -3
  209. package/lib/site-origin.ts +67 -0
  210. package/lib/use-reference-review-data.ts +44 -0
  211. package/middleware.ts +14 -3
  212. package/package.json +56 -3
  213. package/routes.manifest.json +215 -2
  214. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  215. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
  216. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
  217. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -0,0 +1,93 @@
1
+ import type { AutomationOverview, AutomationRun } from "./automation-overview";
2
+
3
+ const NOT_RECORDED = "not recorded for this run";
4
+ const ESTIMATED = "estimated from the model usage reported by the run";
5
+
6
+ function run(fields: Partial<AutomationRun> & Pick<AutomationRun, "status" | "startedAt">): AutomationRun {
7
+ return {
8
+ origin: "monitor", requestedBy: null, reason: null, sourceId: null, referenceId: null, triggerStatus: fields.status, runId: null,
9
+ finishedAt: null, durationSeconds: null, error: null, runRecord: "none", storyId: null, storyVersionId: null, storyTitle: null,
10
+ costUsd: null, costNote: NOT_RECORDED, budgetUsd: null, overBudget: false, usage: null, ...fields,
11
+ };
12
+ }
13
+
14
+ /** A realistic overview for tests and the `?demo=1` newsroom: one live monitor automation and one stored schedule. */
15
+ export const FIXTURE_AUTOMATION_OVERVIEW: AutomationOverview = {
16
+ state: "ready",
17
+ asOf: "2026-10-23T12:00:00Z",
18
+ monitoring: "ready",
19
+ automations: [
20
+ {
21
+ id: "example.ttab.ruling-analysis",
22
+ title: "Analyze new TTAB rulings",
23
+ description: "Drafts a story for each new Board decision.",
24
+ enabled: true,
25
+ enabledNote: "the router starts nothing for a disabled automation",
26
+ trigger: { kind: "monitor", config: { monitorId: "ttab", publicationId: "example" } },
27
+ supported: true,
28
+ supportNote: null,
29
+ procedureKey: "example.ttab.ruling-analysis",
30
+ inputTemplate: { proceeding: "{sourceIdPrefix}", opinion_url: "{documentUrl}" },
31
+ limits: { timeoutSeconds: 840, maxConcurrent: 2, budgetUsdPerRun: 0.05 },
32
+ limitsNote: "the timeout and the number at a time (when set) are enforced when a run starts; the budget is advisory and only flagged",
33
+ runNow: { available: true, reason: null, needsInput: true, inputNames: ["opinion_url", "proceeding"] },
34
+ createdAt: "2026-10-23T10:00:00Z",
35
+ updatedAt: "2026-10-23T10:00:00Z",
36
+ link: { state: "linked", text: "The router's trigger matches this record.", routerMode: "live", routerEnabled: true },
37
+ notice: null,
38
+ skippedAlreadyHadReference: 2,
39
+ lastRun: null,
40
+ runs: [
41
+ run({ sourceId: "91000001-EXA_26", referenceId: "ref-1", runId: "run-1", status: "completed", triggerStatus: "completed",
42
+ startedAt: "2026-10-23T11:30:00Z", finishedAt: "2026-10-23T11:33:10Z", durationSeconds: 190, runRecord: "found",
43
+ storyId: "story-1", storyVersionId: "version-1", storyTitle: "Board affirms a refusal for likelihood of confusion",
44
+ costUsd: 0.0123, costNote: ESTIMATED, budgetUsd: 0.05,
45
+ usage: { calls: 4, promptTokens: 61000, completionTokens: 5200, models: { "gpt-6-luna": 4 }, complete: true, unpricedModels: [] } }),
46
+ run({ sourceId: "91000002-EXA_26", referenceId: "ref-2", runId: "run-2", status: "failed", triggerStatus: "failed",
47
+ startedAt: "2026-10-23T05:30:00Z", finishedAt: "2026-10-23T05:31:00Z", durationSeconds: 60, runRecord: "found",
48
+ error: "the opinion could not be downloaded" }),
49
+ run({ sourceId: "91000003-EXA_26", referenceId: "ref-3", status: "started", startedAt: "2026-10-23T11:59:00Z" }),
50
+ run({ origin: "manual", requestedBy: "ryan@example.test", runId: "run-4", status: "completed", triggerStatus: "completed",
51
+ startedAt: "2026-10-23T09:00:00Z", finishedAt: "2026-10-23T09:05:00Z", durationSeconds: 300, runRecord: "found",
52
+ costUsd: 0.081, costNote: ESTIMATED, budgetUsd: 0.05, overBudget: true }),
53
+ run({ sourceId: "91000005-EXA_26", referenceId: "ref-5", status: "deferred", startedAt: "2026-10-23T08:00:00Z",
54
+ reason: "deferred: concurrency limit: 2 running, at most 2 at a time" }),
55
+ run({ sourceId: "91000006-EXA_26", referenceId: "ref-6", status: "skipped", startedAt: "2026-10-22T08:00:00Z",
56
+ reason: "automation disabled" }),
57
+ ],
58
+ outsideRuns: [
59
+ { runId: "run-9", status: "completed", startedAt: "2026-10-22T15:00:00Z", finishedAt: "2026-10-22T15:02:00Z", durationSeconds: 120,
60
+ error: null, requestedBy: "papyrus-cli", storyId: null, storyVersionId: null, storyTitle: null, costUsd: null,
61
+ costNote: NOT_RECORDED, overBudget: false },
62
+ ],
63
+ outsideNotice: null,
64
+ },
65
+ {
66
+ id: "example.weekly-digest",
67
+ title: "Weekly digest",
68
+ description: null,
69
+ enabled: false,
70
+ enabledNote: "the router starts nothing for a disabled automation",
71
+ trigger: { kind: "schedule", config: { cron: "0 9 * * 1" } },
72
+ supported: false,
73
+ supportNote: "not yet supported",
74
+ procedureKey: "example.weekly-digest",
75
+ inputTemplate: {},
76
+ limits: { timeoutSeconds: 840 },
77
+ limitsNote: "the timeout and the number at a time (when set) are enforced when a run starts; the budget is advisory and only flagged",
78
+ runNow: { available: false, reason: "not yet supported", needsInput: false, inputNames: [] },
79
+ createdAt: "2026-10-23T10:05:00Z",
80
+ updatedAt: "2026-10-23T10:05:00Z",
81
+ link: { state: "not_applicable", text: "not yet supported" },
82
+ notice: "not yet supported",
83
+ skippedAlreadyHadReference: 0,
84
+ lastRun: null,
85
+ runs: [],
86
+ outsideRuns: [],
87
+ outsideNotice: null,
88
+ },
89
+ ],
90
+ };
91
+ // Newest first, as the history is served.
92
+ FIXTURE_AUTOMATION_OVERVIEW.automations[0].runs.sort((left, right) => String(right.startedAt).localeCompare(String(left.startedAt)));
93
+ FIXTURE_AUTOMATION_OVERVIEW.automations[0].lastRun = FIXTURE_AUTOMATION_OVERVIEW.automations[0].runs[0];
@@ -0,0 +1,252 @@
1
+ /** Shapes and wording for the newsroom Automation page. src/papyrus_content/automations.py produces them. */
2
+ import { formatUtc } from "./monitoring-overview";
3
+ import { getNewsroomNavHref } from "./newsroom-nav";
4
+
5
+ export { formatUtc };
6
+
7
+ export type AutomationUsage = {
8
+ calls?: number;
9
+ promptTokens?: number;
10
+ completionTokens?: number;
11
+ models?: Record<string, number>;
12
+ complete?: boolean;
13
+ unpricedModels?: string[];
14
+ };
15
+
16
+ export type AutomationRun = {
17
+ origin: "monitor" | "manual" | "stage";
18
+ /** The stage a story entered to start this run (origin "stage"). */
19
+ stage?: string | null;
20
+ requestedBy: string | null;
21
+ reason: string | null;
22
+ sourceId: string | null;
23
+ referenceId: string | null;
24
+ triggerStatus: string | null;
25
+ runId: string | null;
26
+ status: string | null;
27
+ startedAt: string | null;
28
+ finishedAt: string | null;
29
+ durationSeconds: number | null;
30
+ error: string | null;
31
+ runRecord: "found" | "missing" | "unavailable" | "none";
32
+ storyId: string | null;
33
+ /** False when no version of the story remains (it was deleted); null when this was not looked up. */
34
+ storyExists?: boolean | null;
35
+ /** Tool name to number of calls the procedure made, when the run recorded them. */
36
+ toolsUsed?: Record<string, number> | null;
37
+ storyVersionId: string | null;
38
+ storyTitle: string | null;
39
+ costUsd: number | null;
40
+ costNote: string;
41
+ budgetUsd: number | null;
42
+ overBudget: boolean;
43
+ usage: AutomationUsage | null;
44
+ };
45
+
46
+ /** A ProcedureRun of the same procedure that this automation did not start ("Started outside this automation"). */
47
+ export type AutomationOutsideRun = {
48
+ runId: string | null;
49
+ status: string | null;
50
+ startedAt: string | null;
51
+ finishedAt: string | null;
52
+ durationSeconds: number | null;
53
+ error: string | null;
54
+ requestedBy: string | null;
55
+ storyId: string | null;
56
+ storyVersionId: string | null;
57
+ storyTitle: string | null;
58
+ costUsd: number | null;
59
+ costNote: string;
60
+ overBudget: boolean;
61
+ };
62
+
63
+ export type AutomationRunNow = { available: boolean; reason: string | null; needsInput: boolean; inputNames: string[] };
64
+
65
+ export type AutomationLink = {
66
+ state: "linked" | "input_differs" | "trigger_not_found" | "publication_not_in_router" | "router_not_found" | "not_applicable";
67
+ text: string;
68
+ routerMode?: string | null;
69
+ routerEnabled?: boolean;
70
+ };
71
+
72
+ export type AutomationNotice = "monitoring_not_configured" | "monitoring_unavailable" | "other_publication" | "publication_not_routed" | "not yet supported";
73
+
74
+ export type AutomationRecord = {
75
+ id: string;
76
+ title: string;
77
+ description: string | null;
78
+ enabled: boolean;
79
+ enabledNote: string;
80
+ trigger: { kind: "monitor" | "schedule" | "event" | "manual"; config: Record<string, unknown> };
81
+ /** Story-stage automations only: the stage that fires it, where the engine moves the story after, and the bot-hop limit. */
82
+ onStage?: string | null;
83
+ nextStage?: string | null;
84
+ maxHops?: number | null;
85
+ supported: boolean;
86
+ supportNote: string | null;
87
+ procedureKey: string;
88
+ inputTemplate: Record<string, string>;
89
+ limits: { timeoutSeconds?: number; maxConcurrent?: number; budgetUsdPerRun?: number };
90
+ limitsNote: string;
91
+ runNow: AutomationRunNow;
92
+ createdAt: string | null;
93
+ updatedAt: string | null;
94
+ link: AutomationLink | null;
95
+ runs: AutomationRun[];
96
+ lastRun: AutomationRun | null;
97
+ skippedAlreadyHadReference: number;
98
+ notice: AutomationNotice | null;
99
+ outsideRuns: AutomationOutsideRun[];
100
+ outsideNotice: "outside_unavailable" | null;
101
+ };
102
+
103
+ export type AutomationOverview = {
104
+ state: "ready" | "empty";
105
+ asOf: string;
106
+ monitoring: "ready" | "not_configured";
107
+ automations: AutomationRecord[];
108
+ };
109
+
110
+ export type AutomationResult = AutomationOverview | { state: "unavailable" } | { error: "forbidden" | "bad_request" };
111
+
112
+ const KIND_WORDS: Record<string, string> = { monitor: "Monitor", schedule: "Schedule", event: "Event", manual: "Manual" };
113
+
114
+ export function triggerText(trigger: AutomationRecord["trigger"]): string {
115
+ const kind = KIND_WORDS[trigger.kind] ?? trigger.kind;
116
+ if (trigger.kind === "monitor") {
117
+ return `${kind} ${String(trigger.config.monitorId ?? "?")}, delivering to ${String(trigger.config.publicationId ?? "?")}`;
118
+ }
119
+ if (trigger.kind === "event" && trigger.config.event === "story.stage_entered") {
120
+ const next = trigger.config.nextStage ? `, then moves it to ${String(trigger.config.nextStage)}` : "";
121
+ return `${kind}: a story enters ${String(trigger.config.onStage ?? "?")}${next}`;
122
+ }
123
+ return kind;
124
+ }
125
+
126
+ export const ENABLED_NOTE = "the router starts nothing for a disabled automation";
127
+
128
+ export function enabledText(record: Pick<AutomationRecord, "enabled">): string {
129
+ return record.enabled ? "Enabled" : "Disabled";
130
+ }
131
+
132
+ const STATUS_WORDS: Record<string, string> = {
133
+ completed: "Completed",
134
+ failed: "Failed",
135
+ running: "Running",
136
+ starting: "Starting",
137
+ started: "Started, no result recorded yet",
138
+ startFailed: "Could not start",
139
+ notRunnable: "Not runnable",
140
+ notTriggered: "Not started",
141
+ skipped: "Skipped",
142
+ refused: "Refused",
143
+ deferred: "Deferred, waiting for room",
144
+ };
145
+ const STATUS_MARKS: Record<string, string> = {
146
+ completed: "✓", failed: "✕", startFailed: "✕", notRunnable: "✕", refused: "✕", skipped: "–", deferred: "…", started: "…", starting: "…", running: "…",
147
+ };
148
+
149
+ export function statusText(status: string | null | undefined): string {
150
+ return status ? (STATUS_WORDS[status] ?? status) : "No runs";
151
+ }
152
+
153
+ export function statusMark(status: string | null | undefined): string {
154
+ return status ? (STATUS_MARKS[status] ?? "") : "";
155
+ }
156
+
157
+ export function durationText(seconds: number | null | undefined): string {
158
+ if (seconds === null || seconds === undefined) return "duration not recorded";
159
+ if (seconds < 90) return `${Math.round(seconds)} s`;
160
+ return `${Math.floor(seconds / 60)} min ${Math.round(seconds % 60)} s`;
161
+ }
162
+
163
+ export function dollars(value: number): string {
164
+ return value < 0.01 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
165
+ }
166
+
167
+ /** "Cost: $0.04 (estimated ...)" or "Cost: unknown (why)", with the advisory budget flagged in words. */
168
+ export function costText(run: Pick<AutomationRun, "costUsd" | "costNote" | "overBudget"> & { budgetUsd?: number | null }): string {
169
+ const base = run.costUsd === null ? `Cost: unknown (${run.costNote})` : `Cost: ${dollars(run.costUsd)} (${run.costNote})`;
170
+ return run.overBudget && run.budgetUsd != null ? `${base}. Over the budget of ${dollars(run.budgetUsd)} per run` : base;
171
+ }
172
+
173
+ export function toolsText(run: Pick<AutomationRun, "toolsUsed">): string {
174
+ const tools = Object.entries(run.toolsUsed ?? {}).sort(([a], [b]) => a.localeCompare(b));
175
+ return tools.length ? `Tools used: ${tools.map(([name, count]) => `${name} x${count}`).join(", ")}` : "Tools used: none recorded";
176
+ }
177
+
178
+ export function whoText(run: Pick<AutomationRun, "origin" | "requestedBy" | "sourceId"> & { stage?: string | null }): string {
179
+ if (run.origin === "stage") return `Story entered ${run.stage ?? "a stage"}`;
180
+ return run.origin === "manual" ? `Run now by ${run.requestedBy ?? "someone"}` : `Item: ${run.sourceId ?? "unknown"}`;
181
+ }
182
+
183
+ export const LIMITS_NOTE = "the timeout and the number at a time (when set) are enforced when a run starts; the budget is advisory and only flagged";
184
+
185
+ export function atATimeText(limits: { maxConcurrent?: number }): string {
186
+ return limits.maxConcurrent === undefined ? "no limit on how many at a time" : `${limits.maxConcurrent} at a time`;
187
+ }
188
+
189
+ export type RunNowResult =
190
+ | { state: "started"; automationId: string; requestedBy: string }
191
+ | { state: "refused"; reason: string }
192
+ | { state: "unavailable" }
193
+ | { error: "forbidden" | "bad_request" };
194
+
195
+ export function runNowMessage(result: RunNowResult): string {
196
+ if ("error" in result) return result.error === "forbidden" ? "Run now is for editors and admins." : "The request was not understood.";
197
+ if (result.state === "started") return "Started. The run appears in the list below within a minute; the page refreshes by itself.";
198
+ if (result.state === "refused") return `Not started: ${result.reason}`;
199
+ return "Not started: the request could not be completed right now. Nothing was started.";
200
+ }
201
+
202
+ /** The input a person typed for Run now: a JSON object, or nothing (the record's own input is used). */
203
+ export function parseRunInput(text: string): { ok: true; value: Record<string, unknown> | null } | { ok: false; message: string } {
204
+ if (!text.trim()) return { ok: true, value: null };
205
+ try {
206
+ const parsed = JSON.parse(text);
207
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return { ok: false, message: "The input must be a JSON object, for example {\"name\": \"value\"}." };
208
+ return { ok: true, value: parsed as Record<string, unknown> };
209
+ } catch {
210
+ return { ok: false, message: "The input is not valid JSON." };
211
+ }
212
+ }
213
+
214
+ /** A starting point for the input box: the record's input names with empty values. */
215
+ export function inputTemplateText(names: string[]): string {
216
+ return JSON.stringify(Object.fromEntries(names.map((name) => [name, ""])), null, 2);
217
+ }
218
+
219
+ const LINK_WORDS: Record<AutomationLink["state"], string> = {
220
+ linked: "Linked: the router's trigger matches this record.",
221
+ input_differs: "Differs: the router's input template is not the same as this record's.",
222
+ trigger_not_found: "Not linked: the router has no trigger for this procedure.",
223
+ publication_not_in_router: "Not linked: the router does not deliver to this publication.",
224
+ router_not_found: "Not linked: the monitor has no router configuration.",
225
+ not_applicable: "Not yet supported",
226
+ };
227
+
228
+ export function linkText(link: AutomationLink | null): string {
229
+ return link ? LINK_WORDS[link.state] ?? link.text : "Not checked";
230
+ }
231
+
232
+ const NOTICE_WORDS: Record<string, string> = {
233
+ monitoring_not_configured: "Monitoring is not connected to this site's account, so run history cannot be shown.",
234
+ monitoring_unavailable: "Run history could not be read right now. The record is shown without it.",
235
+ other_publication: "This automation belongs to another publication, so its runs are not shown here.",
236
+ publication_not_routed: "This publication is not one of the router's outputs, so there are no runs to show.",
237
+ "not yet supported": "Only monitor automations run today. This one is stored but nothing starts it.",
238
+ };
239
+
240
+ export const OUTSIDE_NOTICE = "The procedure's other runs could not be read right now.";
241
+
242
+ export function noticeText(notice: AutomationNotice | null): string | null {
243
+ return notice ? (NOTICE_WORDS[notice] ?? notice) : null;
244
+ }
245
+
246
+ export function storyHref(storyId: string, demo?: boolean): string {
247
+ return getNewsroomNavHref(`/newsroom/articles/${encodeURIComponent(storyId)}`, demo);
248
+ }
249
+
250
+ export function proceduresHref(demo?: boolean): string {
251
+ return getNewsroomNavHref("/newsroom/administration/procedures", demo);
252
+ }
@@ -200,7 +200,6 @@ export type ProcedureRunRecord = {
200
200
  newsroomFeedKey?: string | null;
201
201
  };
202
202
 
203
- const DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-newsroom-sections.yml");
204
203
  const NEWSROOM_SECTION_TYPES = new Set<NewsroomSectionType>(["canonical", "floating", "rotating"]);
205
204
  type NewsroomSectionSeedRow = Omit<NewsroomSectionRecord, "sortOrder" | "enabled" | "enabledStatus" | "createdAt" | "updatedAt"> & {
206
205
  enabled: boolean;
@@ -690,10 +689,10 @@ export type CategorySteeringDashboard = {
690
689
  loadError?: string | null;
691
690
  };
692
691
 
693
- const DEFAULT_ANALYSIS_PROFILES_PATH = path.join(process.cwd(), "corpora", "papyrus-analysis-profiles.yml");
694
- const DEFAULT_STEERING_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-steering.yml");
695
692
 
696
- export async function loadAnalysisProfileSummaries(filepath = DEFAULT_ANALYSIS_PROFILES_PATH): Promise<AnalysisProfileSummary[]> {
693
+ /** Analysis profiles the site keeps in a file; a site that configures none (data-driven sites) has none. No default file is looked up. */
694
+ export async function loadAnalysisProfileSummaries(filepath?: string): Promise<AnalysisProfileSummary[]> {
695
+ if (!filepath) return [];
697
696
  try {
698
697
  const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
699
698
  schemaVersion?: number;
@@ -722,7 +721,9 @@ export async function loadAnalysisProfileSummaries(filepath = DEFAULT_ANALYSIS_P
722
721
  }
723
722
  }
724
723
 
725
- export async function loadConfiguredCorpusSummaries(filepath = DEFAULT_STEERING_CONFIG_PATH): Promise<CategorySteeringCorpus[]> {
724
+ /** Corpora a site lists in a steering file; no configured file means none. No default file is looked up. */
725
+ export async function loadConfiguredCorpusSummaries(filepath?: string): Promise<CategorySteeringCorpus[]> {
726
+ if (!filepath) return [];
726
727
  try {
727
728
  const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
728
729
  corpora?: Array<Record<string, unknown>>;
@@ -818,7 +819,8 @@ function sortCategorys(categorys: CategorySteeringCategory[]): CategorySteeringC
818
819
  });
819
820
  }
820
821
 
821
- function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH): NewsroomSectionRecord[] {
822
+ function defaultNewsroomSections(importedAt: string, sectionsConfigPath?: string): NewsroomSectionRecord[] {
823
+ if (!sectionsConfigPath) return [];
822
824
  return loadNewsroomSectionSeedRows(sectionsConfigPath).map((row, index) => ({
823
825
  ...row,
824
826
  enabledStatus: "enabled",
@@ -828,7 +830,7 @@ function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAUL
828
830
  }));
829
831
  }
830
832
 
831
- function loadNewsroomSectionSeedRows(sectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH) {
833
+ function loadNewsroomSectionSeedRows(sectionsConfigPath: string) {
832
834
  const cached = newsroomSectionSeedRowsCache.get(sectionsConfigPath);
833
835
  if (cached) return cached;
834
836
  const parsed = YAML.parse(fs.readFileSync(sectionsConfigPath, "utf8")) as {
@@ -883,7 +885,7 @@ function normalizeNewsroomSectionSeedRow(
883
885
 
884
886
  export function createDemoCategorySteeringDashboard(
885
887
  brandId: SiteBrandId = resolveSiteBrandId(),
886
- newsroomSectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH,
888
+ newsroomSectionsConfigPath?: string,
887
889
  ): CategorySteeringDashboard {
888
890
  const importedAt = "2026-05-16T12:00:00.000Z";
889
891
  const profile = getNewsroomDemoProfile(brandId);
@@ -1502,7 +1504,7 @@ export function createDemoCategorySteeringDashboard(
1502
1504
  riskFlags: ["Avoid promotional framing."],
1503
1505
  coverageGaps: ["Need a second independent source."],
1504
1506
  openQuestions: ["Which users are affected first?"],
1505
- copywriterBrief: "Use accepted evidence and recent desk memory before drafting.",
1507
+ copywriterBrief: "Use accepted evidence and recent section memory before drafting.",
1506
1508
  },
1507
1509
  },
1508
1510
  },
@@ -1652,8 +1654,8 @@ export function createDemoCategorySteeringDashboard(
1652
1654
  slug: "editorial-doctrine-policy",
1653
1655
  title: "Editorial Policy",
1654
1656
  body: [
1655
- "Publication doctrine is the global inclusion standard; desk doctrine sets local source priorities and framing.",
1656
- "News reporting should prioritize current, journalistic, and official sources; desk workflows may use academic research as background unless a desk policy explicitly makes research the primary evidence class.",
1657
+ "Publication doctrine is the global inclusion standard; section doctrine sets local source priorities and framing.",
1658
+ "News reporting should prioritize current, journalistic, and official sources; section workflows may use academic research as background unless a section policy explicitly makes research the primary evidence class.",
1657
1659
  "High-risk domains, including health-related claims, require stricter sourcing, explicit uncertainty handling, and clear separation between verified facts and interpretation.",
1658
1660
  ],
1659
1661
  editorial: { kind: "policy" },
@@ -1698,7 +1700,7 @@ export function createDemoCategorySteeringDashboard(
1698
1700
  question: "Should this candidate source become a reference for this publication?",
1699
1701
  reviewControl: "thumbs",
1700
1702
  reviewReason: "program",
1701
- reviewDetail: "Every decision is reviewed while the cyclotron is onboarding.",
1703
+ reviewDetail: "Every suggestion is reviewed while suggestions are getting started.",
1702
1704
  version: 1,
1703
1705
  }),
1704
1706
  },
@@ -41,7 +41,7 @@ export function formatConsoleWebContextMessage(
41
41
  webPath ? `Web path: ${webPath}` : null,
42
42
  objectUri ? `Focused object: ${objectUri}` : null,
43
43
  label ? `Label: ${label}` : null,
44
- tab ? `Desk: ${tab}` : null,
44
+ tab ? `Section: ${tab}` : null,
45
45
  viewMode ? `View: ${viewMode}` : null,
46
46
  ].filter((line): line is string => Boolean(line));
47
47