@anthusai/papyrus 2.0.0-next.8 → 2.0.0-next.80

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 (211) hide show
  1. package/amplify/data/function-handler.js +20 -0
  2. package/amplify/data/function-handler.ts +40 -0
  3. package/amplify/data/resource.js +151 -61
  4. package/amplify/data/resource.ts +157 -61
  5. package/amplify/data/schema.js +2096 -1980
  6. package/amplify/data/schema.ts +198 -69
  7. package/amplify/functions/assignment-action/data-access.js +14 -15
  8. package/amplify/functions/assignment-action/data-access.ts +13 -14
  9. package/amplify/functions/assignment-action/handler.ts +1 -1
  10. package/amplify/functions/automation-view/handler.py +17 -0
  11. package/amplify/functions/automation-view/resource.js +24 -0
  12. package/amplify/functions/automation-view/resource.ts +34 -0
  13. package/amplify/functions/automation-view/schema.js +26 -0
  14. package/amplify/functions/automation-view/schema.ts +28 -0
  15. package/amplify/functions/automation-view/wire.js +56 -0
  16. package/amplify/functions/automation-view/wire.ts +76 -0
  17. package/amplify/functions/content-actions/handler.py +61 -2
  18. package/amplify/functions/content-actions/requirements.txt +1 -0
  19. package/amplify/functions/content-actions/resource.js +1 -1
  20. package/amplify/functions/content-actions/resource.ts +1 -1
  21. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
  22. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  23. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
  24. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
  25. package/amplify/functions/editorial-checks/schema.js +122 -0
  26. package/amplify/{plugins → functions}/editorial-checks/schema.ts +40 -12
  27. package/amplify/functions/editorial-checks/wire.js +53 -0
  28. package/amplify/functions/editorial-checks/wire.ts +61 -0
  29. package/amplify/functions/monitoring-view/handler.py +11 -0
  30. package/amplify/functions/monitoring-view/resource.js +24 -0
  31. package/amplify/functions/monitoring-view/resource.ts +34 -0
  32. package/amplify/functions/monitoring-view/schema.js +17 -0
  33. package/amplify/functions/monitoring-view/schema.ts +19 -0
  34. package/amplify/functions/monitoring-view/wire.js +37 -0
  35. package/amplify/functions/monitoring-view/wire.ts +47 -0
  36. package/amplify/functions/public-forms/core.js +247 -0
  37. package/amplify/functions/public-forms/core.ts +310 -0
  38. package/amplify/functions/public-forms/handler.ts +41 -0
  39. package/amplify/functions/public-forms/origins.js +35 -0
  40. package/amplify/functions/public-forms/origins.ts +43 -0
  41. package/amplify/functions/public-forms/resource.js +9 -0
  42. package/amplify/functions/public-forms/resource.ts +10 -0
  43. package/amplify/functions/public-forms/wire.js +25 -0
  44. package/amplify/functions/public-forms/wire.ts +32 -0
  45. package/amplify/functions/shared/editorial-context.ts +63 -4
  46. package/amplify/functions/shared/python-bundle.js +20 -14
  47. package/amplify/functions/shared/python-bundle.ts +25 -18
  48. package/amplify/plugins/newsletter/config.js +245 -0
  49. package/amplify/plugins/newsletter/config.ts +305 -0
  50. package/amplify/plugins/newsletter/function/handler.py +6 -0
  51. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  52. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  53. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  54. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  55. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  56. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  57. package/amplify/plugins/newsletter/schema.js +57 -0
  58. package/amplify/plugins/newsletter/schema.ts +62 -0
  59. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  60. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  61. package/amplify/plugins/newsletter/sending.js +140 -0
  62. package/amplify/plugins/newsletter/sending.ts +186 -0
  63. package/amplify/plugins/reader-steering/config.js +182 -0
  64. package/amplify/plugins/reader-steering/config.ts +290 -0
  65. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  66. package/amplify/plugins/reader-steering/function/handler.py +1554 -0
  67. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  68. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  69. package/amplify/plugins/reader-steering/function/proposals.py +255 -0
  70. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  71. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  72. package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
  73. package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
  74. package/amplify/plugins/reader-steering/schema.js +205 -0
  75. package/amplify/plugins/reader-steering/schema.ts +207 -0
  76. package/amplify/plugins/registry.js +4 -2
  77. package/amplify/plugins/registry.ts +4 -2
  78. package/amplify/plugins/signup/schema.js +2 -1
  79. package/amplify/plugins/signup/schema.ts +2 -1
  80. package/amplify/plugins/types.ts +5 -0
  81. package/amplify/plugins/wire.js +1 -0
  82. package/amplify/plugins/wire.ts +1 -0
  83. package/amplify/publication/projection-lease.js +30 -0
  84. package/amplify/publication/projection-lease.ts +39 -0
  85. package/amplify/publication/redirect-and-index.js +105 -0
  86. package/amplify/publication/redirect-rules.js +73 -0
  87. package/amplify/publication/redirect-rules.ts +93 -0
  88. package/amplify/publication/s3-projection.js +7 -1
  89. package/amplify/publication/s3-projection.ts +9 -1
  90. package/amplify/site-backend-config.ts +18 -1
  91. package/amplify/site-backend-names.js +4 -2
  92. package/amplify/site-backend-names.ts +4 -2
  93. package/amplify/site-backend.js +41 -2
  94. package/amplify/site-backend.ts +46 -2
  95. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  96. package/app/api/newsletter/confirm/route.ts +13 -0
  97. package/app/api/newsletter/preferences/route.ts +13 -0
  98. package/app/api/newsletter/state/route.ts +9 -0
  99. package/app/api/newsletter/subscribe/route.ts +9 -0
  100. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  101. package/app/api/newsroom/curation/context/route.ts +9 -0
  102. package/app/api/newsroom/curation/priming/route.ts +9 -0
  103. package/app/api/newsroom/curation/publication/route.ts +9 -0
  104. package/app/api/reader/comment/delete/route.ts +9 -0
  105. package/app/api/reader/comment/edit/route.ts +9 -0
  106. package/app/api/reader/comment/route.ts +9 -0
  107. package/app/api/reader/flag/route.ts +9 -0
  108. package/app/api/reader/handle/route.ts +9 -0
  109. package/app/api/reader/me/route.ts +9 -0
  110. package/app/api/reader/rating/route.ts +9 -0
  111. package/app/api/reader/report/route.ts +9 -0
  112. package/app/api/reader/retired/route.ts +9 -0
  113. package/app/api/reader/submit/route.ts +9 -0
  114. package/app/api/reader/vote/route.ts +9 -0
  115. package/bin/papyrus-infra.mjs +20 -1
  116. package/components/automation-panel.tsx +49 -0
  117. package/components/automation-run-now.tsx +97 -0
  118. package/components/automation-view.tsx +192 -0
  119. package/components/editorial-checks-admin-panel.tsx +36 -11
  120. package/components/moderation-panel.tsx +248 -0
  121. package/components/monitoring-overview-view.tsx +241 -0
  122. package/components/monitoring-panel.tsx +40 -0
  123. package/components/news-desk-client-provider.tsx +18 -18
  124. package/components/news-desk-taxonomy-client.ts +28 -8
  125. package/components/newsletter-panel.tsx +474 -0
  126. package/components/newsletter-sections.tsx +163 -0
  127. package/components/newsroom-access-gate.tsx +15 -1
  128. package/components/newsroom-article-editor.tsx +6 -6
  129. package/components/newsroom-articles-view.tsx +6 -6
  130. package/components/newsroom-board-view.tsx +8 -3
  131. package/components/newsroom-curation-bootstrap.tsx +45 -0
  132. package/components/newsroom-ops-overview.tsx +22 -92
  133. package/components/newsroom-ops-shell.tsx +2 -2
  134. package/components/newsroom-organizer.css +33 -0
  135. package/components/newsroom-overview-skeleton.tsx +25 -0
  136. package/components/newsroom-references-view.tsx +192 -49
  137. package/components/notification-center.tsx +273 -0
  138. package/components/publication-cyclotron-panel.tsx +15 -10
  139. package/components/reader-auth-state.ts +1 -1
  140. package/components/reference-add-entry.tsx +75 -0
  141. package/components/reference-curation-controls.tsx +31 -22
  142. package/components/reference-cyclotron-priming.tsx +58 -19
  143. package/components/reference-cyclotron-suggestion.tsx +10 -6
  144. package/components/reference-priming.tsx +14 -10
  145. package/components/reference-relevance-review.tsx +3 -3
  146. package/components/reference-section-suggestion.tsx +1 -1
  147. package/components/topic-steering-workspace.tsx +223 -144
  148. package/components/use-editorial-checks.ts +12 -10
  149. package/infra/amplify-app-shell.js +94 -16
  150. package/infra/amplify-app-shell.ts +106 -18
  151. package/infra/cloud-email-config.js +32 -0
  152. package/infra/cloud-email-config.ts +39 -0
  153. package/infra/cloud-email.js +57 -0
  154. package/infra/cloud-email.test.js +69 -0
  155. package/infra/cloud-email.test.ts +66 -0
  156. package/infra/cloud-email.ts +60 -0
  157. package/infra/index.js +1 -0
  158. package/infra/secure-string-parameter-handler.js +41 -0
  159. package/infra/secure-string-parameter-handler.ts +41 -0
  160. package/infra/secure-string-parameter.js +79 -0
  161. package/infra/secure-string-parameter.ts +91 -0
  162. package/infra/site-config.js +88 -6
  163. package/infra/site-config.ts +134 -6
  164. package/lib/automation-client.ts +54 -0
  165. package/lib/automation-fixture.ts +93 -0
  166. package/lib/automation-overview.ts +232 -0
  167. package/lib/category-repository.ts +11 -9
  168. package/lib/curation-routes.ts +345 -0
  169. package/lib/cyclotron-suggestions.ts +4 -4
  170. package/lib/editorial-checks-backend.ts +1 -1
  171. package/lib/editorial-checks-client.ts +19 -1
  172. package/lib/editorial-checks-demo.ts +2 -2
  173. package/lib/editorial-checks-settings.ts +3 -0
  174. package/lib/editorial-checks.ts +8 -11
  175. package/lib/editorial-preview.ts +5 -5
  176. package/lib/monitoring-client.ts +29 -0
  177. package/lib/monitoring-overview-fixture.ts +51 -0
  178. package/lib/monitoring-overview.ts +145 -0
  179. package/lib/news-desk-session.ts +12 -21
  180. package/lib/newsletter-api.ts +418 -0
  181. package/lib/newsletter-client.ts +46 -0
  182. package/lib/newsletter-fixture.ts +184 -0
  183. package/lib/newsletter-model.ts +202 -0
  184. package/lib/newsroom-organizer-nav.ts +2 -1
  185. package/lib/prospectus-session.ts +15 -14
  186. package/lib/prospectus-sign-in-routes.ts +84 -19
  187. package/lib/prospectus-site-identity.ts +24 -1
  188. package/lib/reader-api.ts +145 -0
  189. package/lib/reader-moderation.ts +220 -0
  190. package/lib/reader-retirement-route.ts +89 -0
  191. package/lib/reader-subject.md +29 -0
  192. package/lib/reader-subject.ts +53 -0
  193. package/lib/reference-review-details.ts +71 -0
  194. package/lib/reference-review-request.ts +14 -0
  195. package/lib/relevance-decisions.ts +42 -10
  196. package/lib/relevance-status.ts +2 -2
  197. package/lib/review-client.ts +99 -0
  198. package/lib/review-fixture.ts +45 -0
  199. package/lib/review-requests.ts +238 -0
  200. package/lib/semantic-graph.ts +2 -0
  201. package/lib/site-brand-tenant.ts +11 -6
  202. package/lib/site-brand.ts +6 -3
  203. package/lib/site-origin.ts +67 -0
  204. package/lib/use-reference-review-data.ts +44 -0
  205. package/middleware.ts +14 -3
  206. package/package.json +56 -3
  207. package/routes.manifest.json +215 -2
  208. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  209. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
  210. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
  211. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { loadAutomationOverview, runAutomationNow } from "../lib/automation-client";
5
+ import { FIXTURE_AUTOMATION_OVERVIEW } from "../lib/automation-fixture";
6
+ import type { AutomationResult } from "../lib/automation-overview";
7
+ import { AutomationView } from "./automation-view";
8
+
9
+ const REFRESH_MILLISECONDS = 60_000;
10
+
11
+ /** Administration, Automation: loads the records and their recent runs, and refreshes each minute. */
12
+ export function AutomationPanel({ isDemo }: { isDemo: boolean }) {
13
+ const [result, setResult] = useState<AutomationResult | null>(null);
14
+ const [selectedId, setSelectedId] = useState<string | null>(null);
15
+
16
+ useEffect(() => {
17
+ if (isDemo) {
18
+ setResult(FIXTURE_AUTOMATION_OVERVIEW);
19
+ return;
20
+ }
21
+ let live = true;
22
+ const refresh = async () => {
23
+ try {
24
+ const next = await loadAutomationOverview();
25
+ if (live) setResult(next);
26
+ } catch {
27
+ if (live) setResult({ state: "unavailable" });
28
+ }
29
+ };
30
+ void refresh();
31
+ const timer = window.setInterval(() => void refresh(), REFRESH_MILLISECONDS);
32
+ return () => {
33
+ live = false;
34
+ window.clearInterval(timer);
35
+ };
36
+ }, [isDemo]);
37
+
38
+ const runNow = isDemo ? undefined : async (automationId: string, input: Record<string, unknown> | null) => {
39
+ const outcome = await runAutomationNow(automationId, input);
40
+ try {
41
+ setResult(await loadAutomationOverview()); // show the new run without waiting for the minute
42
+ } catch {
43
+ /* the next refresh will */
44
+ }
45
+ return outcome;
46
+ };
47
+
48
+ return <AutomationView result={result} selectedId={selectedId} onSelect={setSelectedId} demo={isDemo} onRunNow={runNow} />;
49
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ atATimeText,
6
+ inputTemplateText,
7
+ parseRunInput,
8
+ runNowMessage,
9
+ type AutomationRecord,
10
+ type RunNowResult,
11
+ } from "../lib/automation-overview";
12
+
13
+ export type RunNowPhase = "idle" | "confirming" | "running" | "done";
14
+
15
+ /**
16
+ * "Run now": a button, then a confirm step that says what will start and, when the record's input names
17
+ * placeholders for an item, asks for the input of this run. Starting costs money (the procedure calls a model),
18
+ * so nothing starts on the first click. `initialPhase` and `initialMessage` exist so every step renders from a test.
19
+ */
20
+ export function RunNowControl({
21
+ record,
22
+ demo = false,
23
+ onRun,
24
+ initialPhase = "idle",
25
+ initialMessage = null,
26
+ }: {
27
+ record: Pick<AutomationRecord, "id" | "title" | "procedureKey" | "runNow" | "limits">;
28
+ demo?: boolean;
29
+ onRun?: (automationId: string, input: Record<string, unknown> | null) => Promise<RunNowResult>;
30
+ initialPhase?: RunNowPhase;
31
+ initialMessage?: string | null;
32
+ }) {
33
+ const [phase, setPhase] = useState<RunNowPhase>(initialPhase);
34
+ const [message, setMessage] = useState<string | null>(initialMessage);
35
+ const [text, setText] = useState(record.runNow.needsInput ? inputTemplateText(record.runNow.inputNames) : "");
36
+ const [problem, setProblem] = useState<string | null>(null);
37
+
38
+ if (!record.runNow.available) {
39
+ return (
40
+ <p className="news-desk-delegation-note" data-automation-run-now-unavailable>
41
+ Run now is not available: {record.runNow.reason === "disabled" ? "the automation is disabled. Enable it first." : (record.runNow.reason ?? "not supported")}.
42
+ </p>
43
+ );
44
+ }
45
+
46
+ async function confirm() {
47
+ const parsed = parseRunInput(text);
48
+ if (!parsed.ok) {
49
+ setProblem(parsed.message);
50
+ return;
51
+ }
52
+ if (record.runNow.needsInput && parsed.value === null) {
53
+ setProblem("This automation's input names an item, so give the input for this run.");
54
+ return;
55
+ }
56
+ setProblem(null);
57
+ setPhase("running");
58
+ try {
59
+ const result = onRun ? await onRun(record.id, parsed.value) : ({ state: "unavailable" } as RunNowResult);
60
+ setMessage(runNowMessage(result));
61
+ } catch {
62
+ setMessage(runNowMessage({ state: "unavailable" }));
63
+ }
64
+ setPhase("done");
65
+ }
66
+
67
+ return (
68
+ <div data-automation-run-now={record.id} data-run-now-phase={phase}>
69
+ {phase === "idle" || phase === "done" ? (
70
+ <button type="button" data-automation-run-now-start onClick={() => { setMessage(null); setPhase("confirming"); }}>
71
+ Run now
72
+ </button>
73
+ ) : null}
74
+ {phase === "confirming" || phase === "running" ? (
75
+ <div role="group" aria-label={`Confirm Run now for ${record.title}`} data-automation-run-now-confirm>
76
+ <p>
77
+ Run <strong>{record.procedureKey}</strong> now? It calls a model, which costs money, and it counts against the
78
+ limit ({atATimeText(record.limits)}). The run is linked to this automation.
79
+ </p>
80
+ {demo ? <p className="news-desk-delegation-note">This is the demo, so nothing will start.</p> : null}
81
+ <label>
82
+ Input for this run{record.runNow.needsInput ? " (required)" : " (optional; blank uses the record's input)"}
83
+ <textarea rows={4} value={text} onChange={(event) => setText(event.target.value)} disabled={phase === "running"} data-automation-run-now-input />
84
+ </label>
85
+ {problem ? <p role="alert" data-automation-run-now-problem>{problem}</p> : null}
86
+ <button type="button" onClick={() => void confirm()} disabled={phase === "running" || demo} data-automation-run-now-confirm-button>
87
+ {phase === "running" ? "Starting…" : "Confirm: start the run"}
88
+ </button>{" "}
89
+ <button type="button" onClick={() => { setPhase("idle"); setProblem(null); }} disabled={phase === "running"} data-automation-run-now-cancel>
90
+ Cancel
91
+ </button>
92
+ </div>
93
+ ) : null}
94
+ {message ? <p role="status" data-automation-run-now-message>{message}</p> : null}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,192 @@
1
+ import Link from "next/link";
2
+ import { RunNowControl } from "./automation-run-now";
3
+ import {
4
+ ENABLED_NOTE,
5
+ OUTSIDE_NOTICE,
6
+ atATimeText,
7
+ costText,
8
+ durationText,
9
+ enabledText,
10
+ formatUtc,
11
+ linkText,
12
+ noticeText,
13
+ proceduresHref,
14
+ statusMark,
15
+ statusText,
16
+ storyHref,
17
+ triggerText,
18
+ whoText,
19
+ type AutomationOutsideRun,
20
+ type AutomationRecord,
21
+ type AutomationResult,
22
+ type AutomationRun,
23
+ type RunNowResult,
24
+ } from "../lib/automation-overview";
25
+
26
+ function Message({ title, children, kind }: { title: string; children?: React.ReactNode; kind: string }) {
27
+ return (
28
+ <div className="category-steering-empty" role={kind === "loading" ? "status" : "alert"} data-automation-state={kind}>
29
+ <strong>{title}</strong>
30
+ {children ? <p>{children}</p> : null}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ function RunLine({ run, demo }: { run: AutomationRun; demo: boolean }) {
36
+ return (
37
+ <li data-automation-run={run.runId ?? run.sourceId ?? "unlinked"} data-run-status={run.status ?? "unknown"}>
38
+ <strong>{formatUtc(run.startedAt)}</strong>
39
+ <span><span aria-hidden="true">{statusMark(run.status)} </span>Status: {statusText(run.status)} &middot; {durationText(run.durationSeconds)}</span>
40
+ <span>{whoText(run)}</span>
41
+ {run.reason ? <span data-automation-run-reason>Reason: {run.reason}</span> : null}
42
+ {run.error ? <span data-automation-run-error>Error: {run.error}</span> : null}
43
+ <span>
44
+ {run.runId ? <>Run {run.runId} (<Link href={proceduresHref(demo)}>Procedures</Link>)</> : run.triggerStatus === "started" ? "Run record not linked yet" : "No run record"}
45
+ {run.runRecord === "missing" ? " (the run record was not found)" : ""}
46
+ {run.runRecord === "unavailable" ? " (the run record could not be read)" : ""}
47
+ </span>
48
+ <span>
49
+ {run.storyId ? <>Story: <Link href={storyHref(run.storyId, demo)} data-automation-story-link>{run.storyTitle ?? run.storyId}</Link></> : "Story: none recorded"}
50
+ </span>
51
+ <span data-automation-run-cost data-over-budget={run.overBudget || undefined}>
52
+ <span aria-hidden="true">{run.overBudget ? "! " : ""}</span>{costText(run)}
53
+ </span>
54
+ </li>
55
+ );
56
+ }
57
+
58
+ function OutsideRunLine({ run, demo }: { run: AutomationOutsideRun; demo: boolean }) {
59
+ return (
60
+ <li data-automation-outside-run={run.runId ?? "unlinked"} data-run-status={run.status ?? "unknown"}>
61
+ <strong>{formatUtc(run.startedAt)}</strong>
62
+ <span><span aria-hidden="true">{statusMark(run.status)} </span>Status: {statusText(run.status)} &middot; {durationText(run.durationSeconds)}</span>
63
+ <span>Started by: {run.requestedBy ?? "unknown"}</span>
64
+ {run.error ? <span data-automation-run-error>Error: {run.error}</span> : null}
65
+ <span>{run.runId ? <>Run {run.runId} (<Link href={proceduresHref(demo)}>Procedures</Link>)</> : "No run id"}</span>
66
+ <span>
67
+ {run.storyId ? <>Story: <Link href={storyHref(run.storyId, demo)}>{run.storyTitle ?? run.storyId}</Link></> : "Story: none recorded"}
68
+ </span>
69
+ <span data-automation-run-cost>{costText({ ...run, budgetUsd: null })}</span>
70
+ </li>
71
+ );
72
+ }
73
+
74
+ function Detail({ record, demo, onRunNow }: { record: AutomationRecord; demo: boolean; onRunNow?: RunNowHandler }) {
75
+ const notice = noticeText(record.notice);
76
+ return (
77
+ <article className="news-desk-doctrine-card" data-automation-detail={record.id}>
78
+ <div className="news-desk-doctrine-card__header"><div><h3>{record.title}</h3><span>{record.description ?? record.id}</span></div></div>
79
+ <dl className="news-desk-monitoring__usage">
80
+ <div><dt>Trigger</dt><dd>{triggerText(record.trigger)}{record.supported ? "" : ` (${record.supportNote ?? "not yet supported"})`}</dd></div>
81
+ <div><dt>Procedure</dt><dd>{record.procedureKey}</dd></div>
82
+ <div><dt>Input</dt><dd>{Object.keys(record.inputTemplate).length ? Object.entries(record.inputTemplate).map(([name, template]) => `${name} = ${template}`).join("; ") : "none"}</dd></div>
83
+ <div><dt>Enabled</dt><dd>{enabledText(record)} ({ENABLED_NOTE})</dd></div>
84
+ <div><dt>Limits</dt><dd>timeout {record.limits.timeoutSeconds ?? "?"} s, {atATimeText(record.limits)}{record.limits.budgetUsdPerRun !== undefined ? `, about $${record.limits.budgetUsdPerRun} per run, advisory` : ""} ({record.limitsNote})</dd></div>
85
+ <div><dt>Router</dt><dd data-automation-link={record.link?.state ?? "unchecked"}>{linkText(record.link)}</dd></div>
86
+ </dl>
87
+ {notice ? <p className="news-desk-delegation-note" data-automation-notice>{notice}</p> : null}
88
+ <RunNowControl record={record} demo={demo} onRun={onRunNow} />
89
+ <h4>Recent runs</h4>
90
+ {record.runs.length ? (
91
+ <ul className="news-desk-monitoring__list" data-automation-runs>
92
+ {record.runs.map((run) => <RunLine key={run.runId ?? `${run.sourceId}-${run.startedAt}`} run={run} demo={demo} />)}
93
+ </ul>
94
+ ) : <p data-automation-no-runs>No runs yet.</p>}
95
+ {record.skippedAlreadyHadReference > 0 ? (
96
+ <p className="news-desk-delegation-note" data-automation-skipped>
97
+ {record.skippedAlreadyHadReference} {record.skippedAlreadyHadReference === 1 ? "item was" : "items were"} not started because the Reference already existed.
98
+ </p>
99
+ ) : null}
100
+ {record.outsideRuns.length || record.outsideNotice ? (
101
+ <>
102
+ <h4>Started outside this automation</h4>
103
+ <p className="news-desk-delegation-note">Other runs of {record.procedureKey} that this automation did not start.</p>
104
+ {record.outsideNotice ? <p className="news-desk-delegation-note" data-automation-notice>{OUTSIDE_NOTICE}</p> : null}
105
+ {record.outsideRuns.length ? (
106
+ <ul className="news-desk-monitoring__list" data-automation-outside-runs>
107
+ {record.outsideRuns.map((run) => <OutsideRunLine key={run.runId ?? run.startedAt ?? "run"} run={run} demo={demo} />)}
108
+ </ul>
109
+ ) : null}
110
+ </>
111
+ ) : null}
112
+ </article>
113
+ );
114
+ }
115
+
116
+ function ListItem({ record, selected, onSelect }: { record: AutomationRecord; selected: boolean; onSelect: (id: string) => void }) {
117
+ const last = record.lastRun;
118
+ return (
119
+ <li data-automation={record.id}>
120
+ <div>
121
+ <strong>{record.title}</strong>
122
+ <span>{triggerText(record.trigger)}{record.supported ? "" : ` (${record.supportNote ?? "not yet supported"})`}</span>
123
+ </div>
124
+ <span>Procedure: {record.procedureKey}</span>
125
+ <span>{enabledText(record)} ({ENABLED_NOTE})</span>
126
+ <span data-automation-last-run>
127
+ Last run: <span aria-hidden="true">{statusMark(last?.status)} </span>{statusText(last?.status)}{last ? `, ${formatUtc(last.startedAt)}` : ""}
128
+ </span>
129
+ <button type="button" aria-pressed={selected} data-automation-select={record.id} onClick={() => onSelect(record.id)}>
130
+ {selected ? "Showing runs" : "Show runs"}
131
+ </button>
132
+ </li>
133
+ );
134
+ }
135
+
136
+ /** Presentation only: the panel fetches, this renders any result, so every state is testable from fixtures. */
137
+ export type RunNowHandler = (automationId: string, input: Record<string, unknown> | null) => Promise<RunNowResult>;
138
+
139
+ export function AutomationView({
140
+ result,
141
+ selectedId,
142
+ onSelect,
143
+ demo = false,
144
+ onRunNow,
145
+ }: {
146
+ result: AutomationResult | null;
147
+ selectedId: string | null;
148
+ onSelect: (id: string) => void;
149
+ demo?: boolean;
150
+ onRunNow?: RunNowHandler;
151
+ }) {
152
+ let body: React.ReactNode;
153
+ if (result === null) {
154
+ body = <Message kind="loading" title="Loading automations" />;
155
+ } else if ("error" in result) {
156
+ body = result.error === "forbidden"
157
+ ? <Message kind="forbidden" title="Automations are for editors and admins">Your account does not have access to this page. Ask an admin for the editor role.</Message>
158
+ : <Message kind="error" title="Automations could not be read">The request was not understood. Reload the page.</Message>;
159
+ } else if (result.state === "unavailable") {
160
+ body = <Message kind="unavailable" title="Automations could not be read right now">Nothing is affected. Try again in a minute.</Message>;
161
+ } else if (result.state === "empty") {
162
+ body = <Message kind="empty" title="No automations yet">An operator creates one with <code>papyrus automations create</code>. Nothing is configured to run on its own from this page.</Message>;
163
+ } else {
164
+ const selected = result.automations.find((record) => record.id === selectedId) ?? (result.automations.length === 1 ? result.automations[0] : null);
165
+ body = (
166
+ <>
167
+ <article className="news-desk-doctrine-card" data-automation-section="list">
168
+ <div className="news-desk-doctrine-card__header"><div><h3>Automations</h3><span>What starts a procedure by itself</span></div></div>
169
+ <ul className="news-desk-monitoring__list">
170
+ {result.automations.map((record) => <ListItem key={record.id} record={record} selected={selected?.id === record.id} onSelect={onSelect} />)}
171
+ </ul>
172
+ {result.monitoring === "not_configured" ? <p className="news-desk-delegation-note" data-automation-notice>{noticeText("monitoring_not_configured")}</p> : null}
173
+ </article>
174
+ {selected ? <Detail record={selected} demo={demo} onRunNow={onRunNow} /> : <p className="news-desk-delegation-note" data-automation-pick>Choose an automation to see its recent runs.</p>}
175
+ </>
176
+ );
177
+ }
178
+ return (
179
+ <div data-news-desk-admin-panel="automations">
180
+ <article className="news-desk-doctrine-card">
181
+ <div className="news-desk-doctrine-card__header">
182
+ <div>
183
+ <h3>Automation</h3>
184
+ <span>Procedures that start on their own. Editors and admins can run one now; an operator changes the settings with the command line.</span>
185
+ </div>
186
+ </div>
187
+ {result && "asOf" in result ? <p className="news-desk-delegation-note" data-automation-as-of>As of {formatUtc(result.asOf)}. Enabled: {ENABLED_NOTE}.</p> : null}
188
+ </article>
189
+ {body}
190
+ </div>
191
+ );
192
+ }
@@ -12,6 +12,7 @@ import {
12
12
  draftQuestions,
13
13
  importStarterPack,
14
14
  listPolicies,
15
+ listStarterPacks,
15
16
  loadSettings,
16
17
  recalibrate,
17
18
  savePolicy,
@@ -19,9 +20,9 @@ import {
19
20
  tryPolicy,
20
21
  type EditorialPolicyRecord,
21
22
  type EditorialSettings,
23
+ type StarterPackSummary,
22
24
  } from "../lib/editorial-checks-client";
23
25
 
24
- const STARTER_PACK = "ai-writing-tells";
25
26
  const POLICY_KEY = /^[a-z0-9][a-z0-9._-]*$/;
26
27
 
27
28
  type Draft = {
@@ -63,8 +64,8 @@ function lines(text: string): string[] {
63
64
  return text.split("\n").map((line) => line.trim()).filter(Boolean);
64
65
  }
65
66
 
66
- /** The policy as Limatus sees it (its key is the policy id). Throws on invalid detector JSON. */
67
- function limatusPolicy(draft: Draft): Record<string, unknown> {
67
+ /** The policy as Humanus sees it (its key is the policy id). Throws on invalid detector JSON. */
68
+ function humanusPolicy(draft: Draft): Record<string, unknown> {
68
69
  const detectors = JSON.parse(draft.detectorsText || "[]") as unknown;
69
70
  if (!Array.isArray(detectors)) throw new Error("Detectors must be a JSON list.");
70
71
  const violates = lines(draft.violatesText);
@@ -87,6 +88,8 @@ export function EditorialChecksAdminPanel() {
87
88
  const [settings, setSettings] = useState<EditorialSettings>(DEFAULT_EDITORIAL_SETTINGS);
88
89
  const [settingsExist, setSettingsExist] = useState(false);
89
90
  const [draft, setDraft] = useState<Draft | null>(null);
91
+ const [starterPacks, setStarterPacks] = useState<StarterPackSummary[]>([]);
92
+ const [starterPack, setStarterPack] = useState<string>("");
90
93
  const [message, setMessage] = useState<string | null>(null);
91
94
  const [working, setWorking] = useState<string | null>(null);
92
95
  const [tryText, setTryText] = useState("");
@@ -94,7 +97,9 @@ export function EditorialChecksAdminPanel() {
94
97
 
95
98
  const refresh = useCallback(async () => {
96
99
  try {
97
- const [loadedPolicies, loadedSettings] = await Promise.all([listPolicies(), loadSettings()]);
100
+ const [loadedPolicies, loadedSettings, catalog] = await Promise.all([listPolicies(), loadSettings(), listStarterPacks()]);
101
+ setStarterPacks(catalog.packs);
102
+ setStarterPack((current) => current || catalog.packs[0]?.name || "");
98
103
  setPolicies(loadedPolicies);
99
104
  setSettings(loadedSettings.settings);
100
105
  setSettingsExist(loadedSettings.exists);
@@ -128,7 +133,7 @@ export function EditorialChecksAdminPanel() {
128
133
  const key = draft.record.policyKey.trim();
129
134
  if (!POLICY_KEY.test(key)) throw new Error("The key must be lowercase letters, digits, dots, hyphens or underscores.");
130
135
  if (!draft.record.title.trim() || !draft.record.guidance.trim()) throw new Error("A policy needs a title and guidance.");
131
- const policy = limatusPolicy(draft);
136
+ const policy = humanusPolicy(draft);
132
137
  const record: EditorialPolicyRecord = {
133
138
  ...draft.record,
134
139
  id: draft.isNew ? `${draft.record.scope}#${key}` : draft.record.id,
@@ -157,19 +162,38 @@ export function EditorialChecksAdminPanel() {
157
162
  <h4>Policies</h4>
158
163
  <div className="flex flex-wrap gap-2">
159
164
  <Button onClick={() => { setDraft(draftFrom(emptyPolicy(), true)); setTryResult(null); }} size="sm" type="button">New policy</Button>
165
+ <select aria-label="Starter pack" onChange={(event) => setStarterPack(event.target.value)} value={starterPack}>
166
+ {starterPacks.map((pack) => (
167
+ <option key={pack.name} value={pack.name}>
168
+ {pack.title} ({pack.importedCount} of {pack.policyCount} added)
169
+ </option>
170
+ ))}
171
+ </select>
160
172
  <Button
161
- disabled={working !== null}
173
+ disabled={working !== null || !starterPack}
162
174
  onClick={() => run("import", async () => {
163
- const outcome = await importStarterPack(STARTER_PACK);
175
+ const outcome = await importStarterPack(starterPack);
164
176
  await refresh();
165
- return `Starter pack: ${outcome.created} policies added, ${outcome.skipped} already present.`;
177
+ const label = starterPacks.find((pack) => pack.name === starterPack)?.title ?? starterPack;
178
+ return `${label}: ${outcome.created} policies added, ${outcome.skipped} already present.`;
166
179
  })}
167
180
  size="sm"
168
181
  type="button"
169
182
  variant="outline"
170
183
  >
171
- Add AI-writing tells starter pack
184
+ Add starter pack
172
185
  </Button>
186
+ </div>
187
+ {(() => {
188
+ const selected = starterPacks.find((pack) => pack.name === starterPack);
189
+ return selected ? (
190
+ <p className="text-sm text-muted-foreground">
191
+ {selected.title}{selected.version ? ` ${selected.version}` : ""}: {selected.policyCount} policies
192
+ {selected.isDefault ? " (added to every new site)" : ""}. {selected.guidance}
193
+ </p>
194
+ ) : null;
195
+ })()}
196
+ <div className="flex flex-wrap gap-2">
173
197
  <Button
174
198
  disabled={working !== null}
175
199
  onClick={() => run("recalibrate", async () => {
@@ -243,7 +267,7 @@ export function EditorialChecksAdminPanel() {
243
267
  <label>Examples that break it (one per line) <Textarea onChange={(event) => setDraft({ ...draft, violatesText: event.target.value })} rows={3} value={draft.violatesText} /></label>
244
268
  <label>Examples that follow it (one per line) <Textarea onChange={(event) => setDraft({ ...draft, compliesText: event.target.value })} rows={3} value={draft.compliesText} /></label>
245
269
  <label>
246
- Detectors (JSON; see <code>limatus schema policy-pack-v1</code>)
270
+ Detectors (JSON; see <code>humanus schema policy-pack-v1</code>)
247
271
  <Textarea className="font-mono text-xs" onChange={(event) => setDraft({ ...draft, detectorsText: event.target.value })} rows={10} value={draft.detectorsText} />
248
272
  </label>
249
273
  <div className="flex flex-wrap gap-2">
@@ -284,7 +308,7 @@ export function EditorialChecksAdminPanel() {
284
308
  <Button
285
309
  disabled={working !== null || !tryText.trim()}
286
310
  onClick={() => run("try", async () => {
287
- setTryResult(await tryPolicy(limatusPolicy(draft), tryText));
311
+ setTryResult(await tryPolicy(humanusPolicy(draft), tryText));
288
312
  })}
289
313
  size="sm"
290
314
  type="button"
@@ -320,6 +344,7 @@ export function EditorialChecksAdminPanel() {
320
344
  });
321
345
  }}
322
346
  >
347
+ <label><input checked={settings.enabled} data-editorial-enabled onChange={(event) => setSettings({ ...settings, enabled: event.target.checked })} type="checkbox" /> Run editorial checks for this publication (turning this off stops checks and rewrites; policies are kept)</label>
323
348
  <label><input checked={settings.lanes.rules} onChange={(event) => setSettings({ ...settings, lanes: { ...settings.lanes, rules: event.target.checked } })} type="checkbox" /> Rule checks</label>
324
349
  <label><input checked={settings.lanes.classifier} onChange={(event) => setSettings({ ...settings, lanes: { ...settings.lanes, classifier: event.target.checked } })} type="checkbox" /> Judgment checks (decision model)</label>
325
350
  <label><input checked={settings.checkWhileTyping} onChange={(event) => setSettings({ ...settings, checkWhileTyping: event.target.checked })} type="checkbox" /> Check while editors type (after a short pause)</label>