@anthusai/papyrus 5.30.0 → 5.32.0

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 (31) hide show
  1. package/amplify/data/resource.js +19 -0
  2. package/amplify/data/resource.ts +20 -0
  3. package/amplify/data/schema.js +21 -0
  4. package/amplify/data/schema.ts +22 -0
  5. package/amplify/functions/automation-view/handler.py +11 -0
  6. package/amplify/functions/automation-view/resource.js +24 -0
  7. package/amplify/functions/automation-view/resource.ts +34 -0
  8. package/amplify/functions/automation-view/schema.js +16 -0
  9. package/amplify/functions/automation-view/schema.ts +18 -0
  10. package/amplify/functions/automation-view/wire.js +32 -0
  11. package/amplify/functions/automation-view/wire.ts +46 -0
  12. package/amplify/plugins/reader-steering/config.js +8 -1
  13. package/amplify/plugins/reader-steering/config.ts +13 -1
  14. package/amplify/plugins/reader-steering/function/handler.py +209 -18
  15. package/amplify/plugins/reader-steering/function/proposals.py +255 -0
  16. package/amplify/plugins/reader-steering/papyrus.plugin.js +10 -2
  17. package/amplify/plugins/reader-steering/papyrus.plugin.ts +10 -2
  18. package/amplify/plugins/reader-steering/schema.js +65 -1
  19. package/amplify/plugins/reader-steering/schema.ts +65 -1
  20. package/amplify/site-backend.js +4 -0
  21. package/amplify/site-backend.ts +4 -0
  22. package/app/api/reader/flag/route.ts +9 -0
  23. package/components/automation-panel.tsx +39 -0
  24. package/components/automation-view.tsx +149 -0
  25. package/components/topic-steering-workspace.tsx +13 -2
  26. package/lib/automation-client.ts +29 -0
  27. package/lib/automation-fixture.ts +65 -0
  28. package/lib/automation-overview.ts +138 -0
  29. package/lib/reader-api.ts +1 -1
  30. package/package.json +7 -1
  31. package/routes.manifest.json +12 -1
@@ -0,0 +1,65 @@
1
+ import type { AutomationOverview } from "./automation-overview";
2
+
3
+ const COST_NOTE = "not recorded: the procedure runner does not record token use or cost on a ProcedureRun yet";
4
+
5
+ /** A realistic overview for tests and the `?demo=1` newsroom: one live monitor automation and one stored schedule. */
6
+ export const FIXTURE_AUTOMATION_OVERVIEW: AutomationOverview = {
7
+ state: "ready",
8
+ asOf: "2026-10-23T12:00:00Z",
9
+ monitoring: "ready",
10
+ automations: [
11
+ {
12
+ id: "example.ttab.ruling-analysis",
13
+ title: "Analyze new TTAB rulings",
14
+ description: "Drafts a story for each new Board decision.",
15
+ enabled: true,
16
+ enabledNote: "display only until the router honours it",
17
+ trigger: { kind: "monitor", config: { monitorId: "ttab", publicationId: "example" } },
18
+ supported: true,
19
+ supportNote: null,
20
+ procedureKey: "example.ttab.ruling-analysis",
21
+ inputTemplate: { proceeding: "{sourceIdPrefix}", opinion_url: "{documentUrl}" },
22
+ limits: { timeoutSeconds: 840, maxConcurrent: 1 },
23
+ limitsNote: "recorded only: the runner does not enforce these yet",
24
+ createdAt: "2026-10-23T10:00:00Z",
25
+ updatedAt: "2026-10-23T10:00:00Z",
26
+ link: { state: "linked", text: "The router's trigger matches this record.", routerMode: "live", routerEnabled: true },
27
+ notice: null,
28
+ skippedAlreadyHadReference: 2,
29
+ lastRun: null,
30
+ runs: [
31
+ { sourceId: "91000001-EXA_26", referenceId: "ref-1", triggerStatus: "completed", runId: "run-1", status: "completed",
32
+ startedAt: "2026-10-23T11:30:00Z", finishedAt: "2026-10-23T11:33:10Z", durationSeconds: 190, error: null, runRecord: "found",
33
+ storyId: "story-1", storyVersionId: "version-1", storyTitle: "Board affirms a refusal for likelihood of confusion", costUsd: null, costNote: COST_NOTE },
34
+ { sourceId: "91000002-EXA_26", referenceId: "ref-2", triggerStatus: "failed", runId: "run-2", status: "failed",
35
+ startedAt: "2026-10-23T05:30:00Z", finishedAt: "2026-10-23T05:31:00Z", durationSeconds: 60, error: "the opinion could not be downloaded", runRecord: "found",
36
+ storyId: null, storyVersionId: null, storyTitle: null, costUsd: null, costNote: COST_NOTE },
37
+ { sourceId: "91000003-EXA_26", referenceId: "ref-3", triggerStatus: "started", runId: null, status: "started",
38
+ startedAt: "2026-10-23T11:59:00Z", finishedAt: null, durationSeconds: null, error: null, runRecord: "none",
39
+ storyId: null, storyVersionId: null, storyTitle: null, costUsd: null, costNote: COST_NOTE },
40
+ ],
41
+ },
42
+ {
43
+ id: "example.weekly-digest",
44
+ title: "Weekly digest",
45
+ description: null,
46
+ enabled: false,
47
+ enabledNote: "display only until the router honours it",
48
+ trigger: { kind: "schedule", config: { cron: "0 9 * * 1" } },
49
+ supported: false,
50
+ supportNote: "not yet supported",
51
+ procedureKey: "example.weekly-digest",
52
+ inputTemplate: {},
53
+ limits: { timeoutSeconds: 840, maxConcurrent: 1 },
54
+ limitsNote: "recorded only: the runner does not enforce these yet",
55
+ createdAt: "2026-10-23T10:05:00Z",
56
+ updatedAt: "2026-10-23T10:05:00Z",
57
+ link: { state: "not_applicable", text: "not yet supported" },
58
+ notice: "not yet supported",
59
+ skippedAlreadyHadReference: 0,
60
+ lastRun: null,
61
+ runs: [],
62
+ },
63
+ ],
64
+ };
65
+ FIXTURE_AUTOMATION_OVERVIEW.automations[0].lastRun = FIXTURE_AUTOMATION_OVERVIEW.automations[0].runs[2];
@@ -0,0 +1,138 @@
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 AutomationRun = {
8
+ sourceId: string | null;
9
+ referenceId: string | null;
10
+ triggerStatus: string | null;
11
+ runId: string | null;
12
+ status: string | null;
13
+ startedAt: string | null;
14
+ finishedAt: string | null;
15
+ durationSeconds: number | null;
16
+ error: string | null;
17
+ runRecord: "found" | "missing" | "unavailable" | "none";
18
+ storyId: string | null;
19
+ storyVersionId: string | null;
20
+ storyTitle: string | null;
21
+ costUsd: number | null;
22
+ costNote: string;
23
+ };
24
+
25
+ export type AutomationLink = {
26
+ state: "linked" | "input_differs" | "trigger_not_found" | "publication_not_in_router" | "router_not_found" | "not_applicable";
27
+ text: string;
28
+ routerMode?: string | null;
29
+ routerEnabled?: boolean;
30
+ };
31
+
32
+ export type AutomationNotice = "monitoring_not_configured" | "monitoring_unavailable" | "other_publication" | "publication_not_routed" | "not yet supported";
33
+
34
+ export type AutomationRecord = {
35
+ id: string;
36
+ title: string;
37
+ description: string | null;
38
+ enabled: boolean;
39
+ enabledNote: string;
40
+ trigger: { kind: "monitor" | "schedule" | "event" | "manual"; config: Record<string, unknown> };
41
+ supported: boolean;
42
+ supportNote: string | null;
43
+ procedureKey: string;
44
+ inputTemplate: Record<string, string>;
45
+ limits: { timeoutSeconds?: number; maxConcurrent?: number; budgetUsdPerRun?: number };
46
+ limitsNote: string;
47
+ createdAt: string | null;
48
+ updatedAt: string | null;
49
+ link: AutomationLink | null;
50
+ runs: AutomationRun[];
51
+ lastRun: AutomationRun | null;
52
+ skippedAlreadyHadReference: number;
53
+ notice: AutomationNotice | null;
54
+ };
55
+
56
+ export type AutomationOverview = {
57
+ state: "ready" | "empty";
58
+ asOf: string;
59
+ monitoring: "ready" | "not_configured";
60
+ automations: AutomationRecord[];
61
+ };
62
+
63
+ export type AutomationResult = AutomationOverview | { state: "unavailable" } | { error: "forbidden" | "bad_request" };
64
+
65
+ const KIND_WORDS: Record<string, string> = { monitor: "Monitor", schedule: "Schedule", event: "Event", manual: "Manual" };
66
+
67
+ export function triggerText(trigger: AutomationRecord["trigger"]): string {
68
+ const kind = KIND_WORDS[trigger.kind] ?? trigger.kind;
69
+ if (trigger.kind === "monitor") {
70
+ return `${kind} ${String(trigger.config.monitorId ?? "?")}, delivering to ${String(trigger.config.publicationId ?? "?")}`;
71
+ }
72
+ return kind;
73
+ }
74
+
75
+ export const ENABLED_NOTE = "display only until the router honours it";
76
+
77
+ export function enabledText(record: Pick<AutomationRecord, "enabled">): string {
78
+ return record.enabled ? "Enabled" : "Disabled";
79
+ }
80
+
81
+ const STATUS_WORDS: Record<string, string> = {
82
+ completed: "Completed",
83
+ failed: "Failed",
84
+ running: "Running",
85
+ starting: "Starting",
86
+ started: "Started, no result recorded yet",
87
+ startFailed: "Could not start",
88
+ notRunnable: "Not runnable",
89
+ notTriggered: "Not started",
90
+ };
91
+ const STATUS_MARKS: Record<string, string> = { completed: "✓", failed: "✕", startFailed: "✕", notRunnable: "✕", started: "…", starting: "…", running: "…" };
92
+
93
+ export function statusText(status: string | null | undefined): string {
94
+ return status ? (STATUS_WORDS[status] ?? status) : "No runs";
95
+ }
96
+
97
+ export function statusMark(status: string | null | undefined): string {
98
+ return status ? (STATUS_MARKS[status] ?? "") : "";
99
+ }
100
+
101
+ export function durationText(seconds: number | null | undefined): string {
102
+ if (seconds === null || seconds === undefined) return "duration not recorded";
103
+ if (seconds < 90) return `${Math.round(seconds)} s`;
104
+ return `${Math.floor(seconds / 60)} min ${Math.round(seconds % 60)} s`;
105
+ }
106
+
107
+ const LINK_WORDS: Record<AutomationLink["state"], string> = {
108
+ linked: "Linked: the router's trigger matches this record.",
109
+ input_differs: "Differs: the router's input template is not the same as this record's.",
110
+ trigger_not_found: "Not linked: the router has no trigger for this procedure.",
111
+ publication_not_in_router: "Not linked: the router does not deliver to this publication.",
112
+ router_not_found: "Not linked: the monitor has no router configuration.",
113
+ not_applicable: "Not yet supported",
114
+ };
115
+
116
+ export function linkText(link: AutomationLink | null): string {
117
+ return link ? LINK_WORDS[link.state] ?? link.text : "Not checked";
118
+ }
119
+
120
+ const NOTICE_WORDS: Record<string, string> = {
121
+ monitoring_not_configured: "Monitoring is not connected to this site's account, so run history cannot be shown.",
122
+ monitoring_unavailable: "Run history could not be read right now. The record is shown without it.",
123
+ other_publication: "This automation belongs to another publication, so its runs are not shown here.",
124
+ publication_not_routed: "This publication is not one of the router's outputs, so there are no runs to show.",
125
+ "not yet supported": "Only monitor automations run today. This one is stored but nothing starts it.",
126
+ };
127
+
128
+ export function noticeText(notice: AutomationNotice | null): string | null {
129
+ return notice ? (NOTICE_WORDS[notice] ?? notice) : null;
130
+ }
131
+
132
+ export function storyHref(storyId: string, demo?: boolean): string {
133
+ return getNewsroomNavHref(`/newsroom/articles/${encodeURIComponent(storyId)}`, demo);
134
+ }
135
+
136
+ export function proceduresHref(demo?: boolean): string {
137
+ return getNewsroomNavHref("/newsroom/administration/procedures", demo);
138
+ }
package/lib/reader-api.ts CHANGED
@@ -12,7 +12,7 @@ export const READER_BODY_BYTES_BY_ACTION: Partial<Record<ReaderAction, number>>
12
12
  export const READER_MAXIMUM_SUBJECTS = 50;
13
13
  export const READER_SUBJECT_KEY = /^[A-Za-z0-9._:-]{1,200}$/;
14
14
 
15
- export type ReaderAction = "me" | "handle" | "vote" | "rating" | "submit" | "comment" | "comment-edit" | "comment-delete" | "report";
15
+ export type ReaderAction = "me" | "handle" | "vote" | "rating" | "submit" | "comment" | "comment-edit" | "comment-delete" | "report" | "flag";
16
16
 
17
17
  export interface ReaderActionEvent {
18
18
  publication: string;
package/package.json CHANGED
@@ -75,6 +75,8 @@
75
75
  "./components/archive-grid": "./components/archive-grid.tsx",
76
76
  "./components/archive-shell": "./components/archive-shell.tsx",
77
77
  "./components/article-video": "./components/article-video.tsx",
78
+ "./components/automation-panel": "./components/automation-panel.tsx",
79
+ "./components/automation-view": "./components/automation-view.tsx",
78
80
  "./components/blog-page-background": "./components/blog-page-background.tsx",
79
81
  "./components/curation-delegation-client": "./components/curation-delegation-client.ts",
80
82
  "./components/dev-sandbox-editor-sign-in": "./components/dev-sandbox-editor-sign-in.tsx",
@@ -176,6 +178,9 @@
176
178
  "./lib/article-flow": "./lib/article-flow.ts",
177
179
  "./lib/article-frame-solver": "./lib/article-frame-solver.ts",
178
180
  "./lib/articles": "./lib/articles.ts",
181
+ "./lib/automation-client": "./lib/automation-client.ts",
182
+ "./lib/automation-fixture": "./lib/automation-fixture.ts",
183
+ "./lib/automation-overview": "./lib/automation-overview.ts",
179
184
  "./lib/blog-feature-solver": "./lib/blog-feature-solver.ts",
180
185
  "./lib/blog-rhythm": "./lib/blog-rhythm.ts",
181
186
  "./lib/brand-fonts-watch": "./lib/brand-fonts-watch.ts",
@@ -361,6 +366,7 @@
361
366
  "./app/api/reader/comment/delete/route": "./app/api/reader/comment/delete/route.ts",
362
367
  "./app/api/reader/comment/edit/route": "./app/api/reader/comment/edit/route.ts",
363
368
  "./app/api/reader/comment/route": "./app/api/reader/comment/route.ts",
369
+ "./app/api/reader/flag/route": "./app/api/reader/flag/route.ts",
364
370
  "./app/api/reader/handle/route": "./app/api/reader/handle/route.ts",
365
371
  "./app/api/reader/me/route": "./app/api/reader/me/route.ts",
366
372
  "./app/api/reader/rating/route": "./app/api/reader/rating/route.ts",
@@ -433,7 +439,7 @@
433
439
  "optional": true
434
440
  }
435
441
  },
436
- "version": "5.30.0",
442
+ "version": "5.32.0",
437
443
  "dependencies": {
438
444
  "@aws-amplify/adapter-nextjs": "^1.7.3",
439
445
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.30.0",
2
+ "version": "5.32.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",
@@ -295,6 +295,17 @@
295
295
  "runtime": "\"nodejs\""
296
296
  }
297
297
  },
298
+ {
299
+ "file": "app/api/reader/flag/route.ts",
300
+ "hasDefault": false,
301
+ "exports": [
302
+ "POST"
303
+ ],
304
+ "config": {
305
+ "dynamic": "\"force-dynamic\"",
306
+ "runtime": "\"nodejs\""
307
+ }
308
+ },
298
309
  {
299
310
  "file": "app/api/reader/handle/route.ts",
300
311
  "hasDefault": false,