@anthusai/papyrus 2.0.0-next.11 → 2.0.0-next.110

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 (227) 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 +152 -62
  6. package/amplify/data/resource.ts +158 -62
  7. package/amplify/data/schema.js +2101 -1987
  8. package/amplify/data/schema.ts +197 -70
  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 +64 -3
  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/functions/editorial-checks/handler.py +2 -2
  24. package/amplify/functions/editorial-checks/requirements.txt +1 -1
  25. package/amplify/functions/editorial-checks/resource.js +3 -3
  26. package/amplify/functions/editorial-checks/resource.ts +3 -3
  27. package/amplify/functions/editorial-checks/schema.js +38 -8
  28. package/amplify/functions/editorial-checks/schema.ts +38 -8
  29. package/amplify/functions/editorial-checks/wire.js +4 -0
  30. package/amplify/functions/editorial-checks/wire.ts +4 -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 +29 -0
  35. package/amplify/functions/monitoring-view/schema.ts +31 -0
  36. package/amplify/functions/monitoring-view/wire.js +64 -0
  37. package/amplify/functions/monitoring-view/wire.ts +81 -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 +1589 -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 +487 -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 +187 -0
  75. package/amplify/plugins/reader-steering/papyrus.plugin.ts +213 -0
  76. package/amplify/plugins/reader-steering/schema.js +252 -0
  77. package/amplify/plugins/reader-steering/schema.ts +254 -0
  78. package/amplify/plugins/registry.js +4 -0
  79. package/amplify/plugins/registry.ts +4 -0
  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.js +19 -0
  93. package/amplify/site-backend-config.ts +45 -1
  94. package/amplify/site-backend-names.js +4 -2
  95. package/amplify/site-backend-names.ts +4 -2
  96. package/amplify/site-backend.js +42 -4
  97. package/amplify/site-backend.ts +45 -4
  98. package/app/.well-known/papyrus-cli.json/route.ts +9 -0
  99. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  100. package/app/api/newsletter/confirm/route.ts +13 -0
  101. package/app/api/newsletter/preferences/route.ts +13 -0
  102. package/app/api/newsletter/state/route.ts +9 -0
  103. package/app/api/newsletter/subscribe/route.ts +9 -0
  104. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  105. package/app/api/newsroom/chat/[...path]/route.ts +17 -0
  106. package/app/api/newsroom/curation/context/route.ts +9 -0
  107. package/app/api/newsroom/curation/priming/route.ts +9 -0
  108. package/app/api/newsroom/curation/publication/route.ts +9 -0
  109. package/app/api/reader/comment/delete/route.ts +9 -0
  110. package/app/api/reader/comment/edit/route.ts +9 -0
  111. package/app/api/reader/comment/route.ts +9 -0
  112. package/app/api/reader/flag/route.ts +9 -0
  113. package/app/api/reader/handle/route.ts +9 -0
  114. package/app/api/reader/me/route.ts +9 -0
  115. package/app/api/reader/rating/route.ts +9 -0
  116. package/app/api/reader/report/route.ts +9 -0
  117. package/app/api/reader/retired/route.ts +9 -0
  118. package/app/api/reader/submit/route.ts +9 -0
  119. package/app/api/reader/vote/route.ts +9 -0
  120. package/app/layout.tsx +5 -1
  121. package/bin/papyrus-infra.mjs +20 -1
  122. package/components/automation-panel.tsx +49 -0
  123. package/components/automation-run-now.tsx +97 -0
  124. package/components/automation-view.tsx +195 -0
  125. package/components/editorial-checks-admin-panel.tsx +32 -17
  126. package/components/moderation-panel.tsx +248 -0
  127. package/components/monitoring-overview-view.tsx +422 -0
  128. package/components/monitoring-panel.tsx +60 -0
  129. package/components/news-desk-client-provider.tsx +18 -18
  130. package/components/news-desk-taxonomy-client.ts +28 -8
  131. package/components/newsletter-panel.tsx +474 -0
  132. package/components/newsletter-sections.tsx +163 -0
  133. package/components/newsroom-access-gate.tsx +16 -2
  134. package/components/newsroom-article-editor.tsx +9 -7
  135. package/components/newsroom-articles-view.tsx +6 -6
  136. package/components/newsroom-assignment-detail.tsx +6 -6
  137. package/components/newsroom-board-view.tsx +8 -3
  138. package/components/newsroom-curation-bootstrap.tsx +45 -0
  139. package/components/newsroom-ops-overview.tsx +26 -96
  140. package/components/newsroom-ops-shell.tsx +2 -2
  141. package/components/newsroom-organizer.css +33 -0
  142. package/components/newsroom-overview-skeleton.tsx +25 -0
  143. package/components/newsroom-references-view.tsx +192 -49
  144. package/components/notification-center.tsx +281 -0
  145. package/components/papyrus-console-conversation.css +14 -0
  146. package/components/papyrus-console-conversation.tsx +55 -0
  147. package/components/papyrus-console-shell.tsx +62 -420
  148. package/components/papyrus-private-chat-panel.tsx +134 -0
  149. package/components/publication-cyclotron-panel.tsx +15 -10
  150. package/components/reader-auth-state.ts +1 -1
  151. package/components/reference-add-entry.tsx +75 -0
  152. package/components/reference-curation-controls.tsx +31 -22
  153. package/components/reference-cyclotron-priming.tsx +58 -19
  154. package/components/reference-cyclotron-suggestion.tsx +10 -6
  155. package/components/reference-priming.tsx +14 -10
  156. package/components/reference-relevance-review.tsx +3 -3
  157. package/components/reference-section-suggestion.tsx +1 -1
  158. package/components/topic-steering-workspace.tsx +230 -144
  159. package/infra/amplify-app-shell.js +96 -16
  160. package/infra/amplify-app-shell.ts +108 -18
  161. package/infra/cloud-email-config.js +32 -0
  162. package/infra/cloud-email-config.ts +39 -0
  163. package/infra/cloud-email.js +57 -0
  164. package/infra/cloud-email.test.js +69 -0
  165. package/infra/cloud-email.test.ts +66 -0
  166. package/infra/cloud-email.ts +60 -0
  167. package/infra/index.js +1 -0
  168. package/infra/secure-string-parameter-handler.js +41 -0
  169. package/infra/secure-string-parameter-handler.ts +41 -0
  170. package/infra/secure-string-parameter.js +79 -0
  171. package/infra/secure-string-parameter.ts +91 -0
  172. package/infra/site-config.js +92 -6
  173. package/infra/site-config.ts +138 -6
  174. package/lib/automation-client.ts +54 -0
  175. package/lib/automation-fixture.ts +93 -0
  176. package/lib/automation-overview.ts +252 -0
  177. package/lib/category-repository.ts +14 -12
  178. package/lib/cli-login-discovery.ts +78 -0
  179. package/lib/console-web-context.ts +1 -1
  180. package/lib/curation-routes.ts +345 -0
  181. package/lib/cyclotron-suggestions.ts +4 -4
  182. package/lib/editorial-checks-client.ts +18 -0
  183. package/lib/editorial-checks-demo.ts +1 -1
  184. package/lib/editorial-checks.ts +8 -4
  185. package/lib/editorial-preview.ts +5 -5
  186. package/lib/monitoring-client.ts +83 -0
  187. package/lib/monitoring-overview-fixture.ts +51 -0
  188. package/lib/monitoring-overview.ts +191 -0
  189. package/lib/news-desk-session.ts +12 -21
  190. package/lib/newsletter-api.ts +418 -0
  191. package/lib/newsletter-client.ts +46 -0
  192. package/lib/newsletter-fixture.ts +184 -0
  193. package/lib/newsletter-model.ts +202 -0
  194. package/lib/newsroom-demo-dashboard.ts +1 -1
  195. package/lib/newsroom-nav.ts +1 -1
  196. package/lib/newsroom-organizer-nav.ts +2 -1
  197. package/lib/papyrus-chat-admission.ts +65 -0
  198. package/lib/papyrus-hosted-chat.ts +217 -0
  199. package/lib/papyrus-private-chat-client.ts +81 -0
  200. package/lib/prospectus-session.ts +30 -14
  201. package/lib/prospectus-sign-in-routes.ts +106 -31
  202. package/lib/prospectus-site-identity.ts +46 -6
  203. package/lib/reader-api.ts +145 -0
  204. package/lib/reader-moderation.ts +220 -0
  205. package/lib/reader-retirement-route.ts +89 -0
  206. package/lib/reader-subject.md +29 -0
  207. package/lib/reader-subject.ts +53 -0
  208. package/lib/reference-review-details.ts +71 -0
  209. package/lib/reference-review-request.ts +14 -0
  210. package/lib/relevance-decisions.ts +42 -10
  211. package/lib/relevance-status.ts +2 -2
  212. package/lib/review-client.ts +120 -0
  213. package/lib/review-fixture.ts +45 -0
  214. package/lib/review-requests.ts +246 -0
  215. package/lib/semantic-graph.ts +2 -0
  216. package/lib/shared-chat-admission.ts +182 -0
  217. package/lib/site-brand-tenant.ts +11 -6
  218. package/lib/site-brand.ts +6 -3
  219. package/lib/site-origin-value.ts +50 -0
  220. package/lib/site-origin.ts +8 -0
  221. package/lib/use-reference-review-data.ts +44 -0
  222. package/middleware.ts +14 -3
  223. package/package.json +67 -3
  224. package/routes.manifest.json +238 -2
  225. package/src/sync.mjs +11 -2
  226. package/src/with-papyrus.d.mts +1 -0
  227. package/src/with-papyrus.mjs +17 -1
@@ -0,0 +1,246 @@
1
+ """AI moderation: a model reads each new submission and comment and answers allow or hide, with a reason.
2
+
3
+ The decision is applied as an ordinary moderation action marked ``by: "ai"``, so it appears in the log, shows in the panel,
4
+ and a human can undo it or decide differently. A human decision always stands: an item a person has acted on is never
5
+ judged by the model again. A model that fails, times out or answers something that is not a valid verdict leaves the item
6
+ in the human queue.
7
+
8
+ Secrets: the API key is read from an SSM SecureString at the start of a run and lives only in the object that makes the
9
+ request. It is never logged, never put in a returned value and never in an error message (errors are reported by class name).
10
+ Tests stub the model; nothing in the tests, CI or this module's defaults makes a paid call.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import re
18
+ import time
19
+ import urllib.error
20
+ import urllib.request
21
+ from collections.abc import Callable
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Protocol
24
+
25
+ import moderation as rules
26
+
27
+ PROMPT_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "moderation-prompt.md")
28
+ DEFAULT_MODEL = "gpt-6-luna"
29
+ RESPONSES_URL = "https://api.openai.com/v1/responses"
30
+ ALLOW, HIDE = "allow", "hide"
31
+ MAXIMUM_REASON = 200
32
+ MAXIMUM_TEXT = 4000
33
+ BATCH_LIMIT = 25
34
+ CONSECUTIVE_FAILURES_BEFORE_STOPPING = 3
35
+ REQUEST_TIMEOUT_SECONDS = 20
36
+ AI_ACTOR = "ai"
37
+ REASON_PREFIX = "AI: "
38
+ PARAMETER_NAME = re.compile(r"/papyrus/[a-z0-9][a-z0-9-]{0,62}/openai-api-key")
39
+
40
+
41
+ # Structured Outputs: the API itself constrains the answer to this schema (strict), so no "json" wording is needed in the input.
42
+ VERDICT_FORMAT: dict[str, Any] = {
43
+ "type": "json_schema",
44
+ "name": "moderation_verdict",
45
+ "strict": True,
46
+ "schema": {
47
+ "type": "object",
48
+ "properties": {"verdict": {"type": "string", "enum": [ALLOW, HIDE]}, "reason": {"type": "string"}},
49
+ "required": ["verdict", "reason"],
50
+ "additionalProperties": False,
51
+ },
52
+ }
53
+
54
+
55
+ class ModelOutputError(ValueError):
56
+ pass
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Verdict:
61
+ decision: str
62
+ reason: str
63
+
64
+
65
+ class ModerationModel(Protocol):
66
+ def judge(self, item: dict[str, Any]) -> Verdict: ...
67
+
68
+
69
+ def load_prompt(path: str = PROMPT_FILE) -> str:
70
+ with open(path, encoding="utf-8") as handle:
71
+ return handle.read().strip()
72
+
73
+
74
+ def parse_verdict(text: Any) -> Verdict:
75
+ """A strict reading of the model's answer: one JSON object, ``verdict`` (or the older ``decision``) allow or hide, a short plain reason."""
76
+ if not isinstance(text, str):
77
+ raise ModelOutputError("the model answered nothing")
78
+ start, end = text.find("{"), text.rfind("}")
79
+ if start < 0 or end < start:
80
+ raise ModelOutputError("the model did not answer with a JSON object")
81
+ try:
82
+ data = json.loads(text[start : end + 1])
83
+ except ValueError as error:
84
+ raise ModelOutputError("the model's answer is not valid JSON") from error
85
+ if not isinstance(data, dict):
86
+ raise ModelOutputError("the decision is neither allow nor hide")
87
+ decision, reason = data.get("verdict", data.get("decision")), data.get("reason")
88
+ if decision not in (ALLOW, HIDE):
89
+ raise ModelOutputError("the decision is neither allow nor hide")
90
+ if not isinstance(reason, str) or not reason.strip():
91
+ raise ModelOutputError("the model gave no reason")
92
+ return Verdict(decision, " ".join(reason.split())[:MAXIMUM_REASON])
93
+
94
+
95
+ def response_text(body: Any) -> str | None:
96
+ """The text of a Responses API answer: ``output_text`` when present, else the text parts of the output messages."""
97
+ if not isinstance(body, dict):
98
+ return None
99
+ if isinstance(body.get("output_text"), str):
100
+ return body["output_text"]
101
+ parts = [
102
+ part.get("text")
103
+ for message in body.get("output") or []
104
+ if isinstance(message, dict)
105
+ for part in message.get("content") or []
106
+ if isinstance(part, dict) and isinstance(part.get("text"), str)
107
+ ]
108
+ return "".join(parts) if parts else None
109
+
110
+
111
+ def urllib_transport(url: str, headers: dict[str, str], payload: dict[str, Any]) -> dict[str, Any]:
112
+ request = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST")
113
+ with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT_SECONDS) as response: # noqa: S310 - a fixed https endpoint
114
+ return json.loads(response.read().decode("utf-8"))
115
+
116
+
117
+ class OpenAIModerationModel:
118
+ """GPT-6 Luna (``moderation.ai.model``) through the OpenAI Responses API. The key is held here and nowhere else."""
119
+
120
+ def __init__(
121
+ self,
122
+ api_key: str,
123
+ *,
124
+ model: str = DEFAULT_MODEL,
125
+ prompt: str | None = None,
126
+ transport: Callable[[str, dict[str, str], dict[str, Any]], dict[str, Any]] = urllib_transport,
127
+ ) -> None:
128
+ if not api_key:
129
+ raise ValueError("an API key is required")
130
+ self._api_key = api_key
131
+ self._model = model
132
+ self._prompt = prompt if prompt is not None else load_prompt()
133
+ self._transport = transport
134
+
135
+ def request_payload(self, item: dict[str, Any]) -> dict[str, Any]:
136
+ return {
137
+ "model": self._model,
138
+ "instructions": self._prompt,
139
+ "input": json.dumps(item, ensure_ascii=False, separators=(",", ":")),
140
+ "text": {"format": VERDICT_FORMAT},
141
+ "max_output_tokens": 300,
142
+ "store": False,
143
+ }
144
+
145
+ def judge(self, item: dict[str, Any]) -> Verdict:
146
+ body = self._transport(RESPONSES_URL, {"authorization": f"Bearer {self._api_key}", "content-type": "application/json"}, self.request_payload(item))
147
+ return parse_verdict(response_text(body))
148
+
149
+
150
+ def model_input(kind: str, row: dict[str, Any]) -> dict[str, Any]:
151
+ """What the model sees: the content, the automatic signals and the report count. Never a reader id, a handle or a siteSubject."""
152
+ signals = row.get("signals")
153
+ if isinstance(signals, str):
154
+ try:
155
+ signals = json.loads(signals)
156
+ except ValueError:
157
+ signals = []
158
+ listed = [
159
+ {"policy": str(signal.get("policy", ""))[:60], "detector": str(signal.get("detector", ""))[:60], "severity": str(signal.get("severity", ""))[:12]}
160
+ for signal in (signals if isinstance(signals, list) else [])
161
+ if isinstance(signal, dict)
162
+ ][:20]
163
+ if kind == rules.TARGET_SUBMISSION:
164
+ item: dict[str, Any] = {"kind": "submission", "title": row["title"], "url": row["canonicalUrl"], "domain": row["domain"], "text": row.get("note") or ""}
165
+ else:
166
+ item = {"kind": "comment", "text": row["body"]}
167
+ item["text"] = str(item["text"])[:MAXIMUM_TEXT]
168
+ item["signals"] = listed
169
+ item["reports"] = int(row.get("reportCount") or 0)
170
+ return item
171
+
172
+
173
+ @dataclass
174
+ class AiReport:
175
+ judged: int = 0
176
+ allowed: int = 0
177
+ hidden: int = 0
178
+ skipped_human: int = 0
179
+ errors: dict[str, int] = field(default_factory=dict)
180
+ stopped: bool = False
181
+
182
+ def to_dict(self) -> dict[str, Any]:
183
+ return {"judged": self.judged, "allowed": self.allowed, "hidden": self.hidden, "skippedHuman": self.skipped_human, "errors": dict(self.errors), "stopped": self.stopped}
184
+
185
+
186
+ class AiStore(Protocol):
187
+ def new_content(self) -> list[tuple[str, dict[str, Any]]]: ...
188
+
189
+ def human_acted_on(self) -> set[str]: ...
190
+
191
+
192
+ def run_ai_moderation(
193
+ store: Any,
194
+ model: ModerationModel,
195
+ *,
196
+ now: Callable[[], float],
197
+ settings: rules.ModerationSettings,
198
+ deadline: float,
199
+ limit: int = BATCH_LIMIT,
200
+ iso: Callable[[float], str],
201
+ ) -> AiReport:
202
+ """Judges the oldest new items first, until the batch limit, the deadline, or three failures in a row."""
203
+ report = AiReport()
204
+ human_targets = store.human_acted_on()
205
+ failures_in_a_row = 0
206
+ candidates = sorted(store.new_content(), key=lambda entry: (entry[1]["createdAt"], entry[1]["id"]))
207
+ for kind, row in candidates:
208
+ if report.judged >= limit or now() >= deadline:
209
+ break
210
+ if row["id"] in human_targets:
211
+ report.skipped_human += 1
212
+ continue
213
+ if row.get("state") == "deleted":
214
+ continue
215
+ try:
216
+ verdict = model.judge(model_input(kind, row))
217
+ except Exception as error: # noqa: BLE001 - reported by class name only: a message could carry the key or the content
218
+ report.errors[type(error).__name__] = report.errors.get(type(error).__name__, 0) + 1
219
+ failures_in_a_row += 1
220
+ if failures_in_a_row >= CONSECUTIVE_FAILURES_BEFORE_STOPPING:
221
+ report.stopped = True
222
+ break
223
+ continue
224
+ failures_in_a_row = 0
225
+ action = rules.APPROVE if verdict.decision == ALLOW else rules.HIDE
226
+ outcome = rules.apply_action(
227
+ store,
228
+ action=action,
229
+ target_type=kind,
230
+ target=row["id"],
231
+ reason=REASON_PREFIX + verdict.reason,
232
+ undo_of=None,
233
+ actor_sub=AI_ACTOR,
234
+ by=rules.BY_AI,
235
+ now_iso=iso(now()),
236
+ settings=settings,
237
+ )
238
+ if outcome.status != "ok":
239
+ report.errors[outcome.error or "refused"] = report.errors.get(outcome.error or "refused", 0) + 1
240
+ continue
241
+ report.judged += 1
242
+ if action == rules.APPROVE:
243
+ report.allowed += 1
244
+ else:
245
+ report.hidden += 1
246
+ return report