@anthusai/papyrus 3.3.0 → 5.0.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 (38) hide show
  1. package/amplify/data/schema.js +2 -0
  2. package/amplify/data/schema.ts +2 -0
  3. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +1 -1
  4. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  5. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
  6. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
  7. package/amplify/functions/editorial-checks/schema.js +92 -0
  8. package/amplify/{plugins → functions}/editorial-checks/schema.ts +4 -6
  9. package/amplify/functions/editorial-checks/wire.js +49 -0
  10. package/amplify/functions/editorial-checks/wire.ts +57 -0
  11. package/amplify/plugins/registry.js +0 -2
  12. package/amplify/plugins/registry.ts +0 -2
  13. package/amplify/publication/redirect-and-index.js +31 -0
  14. package/amplify/publication/redirect-rules.js +40 -0
  15. package/amplify/publication/redirect-rules.ts +47 -0
  16. package/amplify/publication/s3-projection.js +3 -1
  17. package/amplify/publication/s3-projection.ts +5 -1
  18. package/amplify/site-backend.js +4 -0
  19. package/amplify/site-backend.ts +4 -0
  20. package/components/editorial-checks-admin-panel.tsx +19 -9
  21. package/components/newsroom-article-editor.tsx +2 -4
  22. package/components/topic-steering-workspace.tsx +10 -17
  23. package/components/use-editorial-checks.ts +12 -10
  24. package/lib/category-repository.ts +10 -8
  25. package/lib/editorial-checks-backend.ts +1 -1
  26. package/lib/editorial-checks-client.ts +1 -1
  27. package/lib/editorial-checks-demo.ts +2 -2
  28. package/lib/editorial-checks-settings.ts +3 -0
  29. package/lib/editorial-checks.ts +2 -9
  30. package/lib/editorial-preview.ts +5 -5
  31. package/lib/site-brand-tenant.ts +11 -6
  32. package/lib/site-brand.ts +6 -3
  33. package/package.json +1 -1
  34. package/routes.manifest.json +1 -1
  35. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  36. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
  37. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
  38. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -21,7 +21,11 @@ import {
21
21
  type EditorialSettings,
22
22
  } from "../lib/editorial-checks-client";
23
23
 
24
- const STARTER_PACK = "ai-writing-tells";
24
+ const STARTER_PACKS = [
25
+ { name: "ai-writing-tells", label: "AI-writing tells" },
26
+ { name: "plain-language", label: "Federal Plain Language Guidelines" },
27
+ { name: "asd-ste100", label: "ASD-STE100 Simplified Technical English" },
28
+ ] as const;
25
29
  const POLICY_KEY = /^[a-z0-9][a-z0-9._-]*$/;
26
30
 
27
31
  type Draft = {
@@ -63,8 +67,8 @@ function lines(text: string): string[] {
63
67
  return text.split("\n").map((line) => line.trim()).filter(Boolean);
64
68
  }
65
69
 
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> {
70
+ /** The policy as Humanus sees it (its key is the policy id). Throws on invalid detector JSON. */
71
+ function humanusPolicy(draft: Draft): Record<string, unknown> {
68
72
  const detectors = JSON.parse(draft.detectorsText || "[]") as unknown;
69
73
  if (!Array.isArray(detectors)) throw new Error("Detectors must be a JSON list.");
70
74
  const violates = lines(draft.violatesText);
@@ -87,6 +91,7 @@ export function EditorialChecksAdminPanel() {
87
91
  const [settings, setSettings] = useState<EditorialSettings>(DEFAULT_EDITORIAL_SETTINGS);
88
92
  const [settingsExist, setSettingsExist] = useState(false);
89
93
  const [draft, setDraft] = useState<Draft | null>(null);
94
+ const [starterPack, setStarterPack] = useState<string>(STARTER_PACKS[0].name);
90
95
  const [message, setMessage] = useState<string | null>(null);
91
96
  const [working, setWorking] = useState<string | null>(null);
92
97
  const [tryText, setTryText] = useState("");
@@ -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,18 +162,22 @@ 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
+ {STARTER_PACKS.map((pack) => <option key={pack.name} value={pack.name}>{pack.label}</option>)}
167
+ </select>
160
168
  <Button
161
169
  disabled={working !== null}
162
170
  onClick={() => run("import", async () => {
163
- const outcome = await importStarterPack(STARTER_PACK);
171
+ const outcome = await importStarterPack(starterPack);
164
172
  await refresh();
165
- return `Starter pack: ${outcome.created} policies added, ${outcome.skipped} already present.`;
173
+ const label = STARTER_PACKS.find((pack) => pack.name === starterPack)?.label ?? starterPack;
174
+ return `${label}: ${outcome.created} policies added, ${outcome.skipped} already present.`;
166
175
  })}
167
176
  size="sm"
168
177
  type="button"
169
178
  variant="outline"
170
179
  >
171
- Add AI-writing tells starter pack
180
+ Add starter pack
172
181
  </Button>
173
182
  <Button
174
183
  disabled={working !== null}
@@ -243,7 +252,7 @@ export function EditorialChecksAdminPanel() {
243
252
  <label>Examples that break it (one per line) <Textarea onChange={(event) => setDraft({ ...draft, violatesText: event.target.value })} rows={3} value={draft.violatesText} /></label>
244
253
  <label>Examples that follow it (one per line) <Textarea onChange={(event) => setDraft({ ...draft, compliesText: event.target.value })} rows={3} value={draft.compliesText} /></label>
245
254
  <label>
246
- Detectors (JSON; see <code>limatus schema policy-pack-v1</code>)
255
+ Detectors (JSON; see <code>humanus schema policy-pack-v1</code>)
247
256
  <Textarea className="font-mono text-xs" onChange={(event) => setDraft({ ...draft, detectorsText: event.target.value })} rows={10} value={draft.detectorsText} />
248
257
  </label>
249
258
  <div className="flex flex-wrap gap-2">
@@ -284,7 +293,7 @@ export function EditorialChecksAdminPanel() {
284
293
  <Button
285
294
  disabled={working !== null || !tryText.trim()}
286
295
  onClick={() => run("try", async () => {
287
- setTryResult(await tryPolicy(limatusPolicy(draft), tryText));
296
+ setTryResult(await tryPolicy(humanusPolicy(draft), tryText));
288
297
  })}
289
298
  size="sm"
290
299
  type="button"
@@ -320,6 +329,7 @@ export function EditorialChecksAdminPanel() {
320
329
  });
321
330
  }}
322
331
  >
332
+ <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
333
  <label><input checked={settings.lanes.rules} onChange={(event) => setSettings({ ...settings, lanes: { ...settings.lanes, rules: event.target.checked } })} type="checkbox" /> Rule checks</label>
324
334
  <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
335
  <label><input checked={settings.checkWhileTyping} onChange={(event) => setSettings({ ...settings, checkWhileTyping: event.target.checked })} type="checkbox" /> Check while editors type (after a short pause)</label>
@@ -48,11 +48,9 @@ import {
48
48
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
49
49
  import { SITE_BRAND } from "../lib/site-brand";
50
50
  import { cn } from "../lib/utils";
51
- import amplifyOutputs from "papyrus-amplify-outputs";
52
- import { applyReplacement, editorialChecksEnabled, inlineFindings, visibleFindings } from "../lib/editorial-checks";
51
+ import { applyReplacement, inlineFindings, visibleFindings } from "../lib/editorial-checks";
53
52
  import { graphqlEditorialBackend } from "../lib/editorial-checks-client";
54
53
  import { demoEditorialBackend } from "../lib/editorial-checks-demo";
55
- import type { PluginOutputs } from "../lib/plugin-settings";
56
54
  import type { FindingActions } from "./editorial-finding-card";
57
55
  import { EditorialFindingsPanel } from "./editorial-findings-panel";
58
56
  import { EditorialPreviewPopover, type PreviewPopoverTarget } from "./editorial-preview-popover";
@@ -219,7 +217,7 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
219
217
  const frontMatterYaml = editor?.frontMatterYaml ?? null;
220
218
  const bodyMarkus = editor?.bodyMarkus ?? null;
221
219
  const editorialBackend = useMemo(
222
- () => (demo ? demoEditorialBackend : !previewMode && editorialChecksEnabled(amplifyOutputs as PluginOutputs) ? graphqlEditorialBackend : null),
220
+ () => (demo ? demoEditorialBackend : !previewMode ? graphqlEditorialBackend : null),
223
221
  [demo, previewMode],
224
222
  );
225
223
  const editorial = useEditorialChecks({
@@ -4318,17 +4318,15 @@ function AdministrationDeskView({
4318
4318
  <strong>Agent delegations</strong>
4319
4319
  <span>Let an agent curate References for you</span>
4320
4320
  </Link>
4321
- {editorialChecksPluginEnabled() ? (
4322
- <Link
4323
- href={administrationPanelHref("editorial-checks", isDemo)}
4324
- data-news-desk-admin-nav="editorial-checks"
4325
- data-active={administrationPanel === "editorial-checks" || undefined}
4326
- onClick={() => onPanelChange("editorial-checks")}
4327
- >
4328
- <strong>Editorial checks</strong>
4329
- <span>Policies the article editor checks drafts against</span>
4330
- </Link>
4331
- ) : null}
4321
+ <Link
4322
+ href={administrationPanelHref("editorial-checks", isDemo)}
4323
+ data-news-desk-admin-nav="editorial-checks"
4324
+ data-active={administrationPanel === "editorial-checks" || undefined}
4325
+ onClick={() => onPanelChange("editorial-checks")}
4326
+ >
4327
+ <strong>Editorial checks</strong>
4328
+ <span>Policies the article editor checks drafts against</span>
4329
+ </Link>
4332
4330
  {signupPluginEnabled() ? (
4333
4331
  <Link
4334
4332
  href={administrationPanelHref("signups", isDemo)}
@@ -12807,14 +12805,9 @@ function signupPluginEnabled(): boolean {
12807
12805
  return pluginSettings(amplifyOutputs as PluginOutputs, "signup") !== null;
12808
12806
  }
12809
12807
 
12810
- function editorialChecksPluginEnabled(): boolean {
12811
- return pluginSettings(amplifyOutputs as PluginOutputs, "editorial-checks") !== null;
12812
- }
12813
-
12814
12808
  function normalizeAdministrationPanel(value: string | null | undefined): AdministrationPanel {
12815
- if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations") return value;
12809
+ if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations" || value === "editorial-checks") return value;
12816
12810
  if (value === "signups" && signupPluginEnabled()) return value;
12817
- if (value === "editorial-checks" && editorialChecksPluginEnabled()) return value;
12818
12811
  return "users";
12819
12812
  }
12820
12813
 
@@ -27,14 +27,16 @@ export function useEditorialChecks({
27
27
  section,
28
28
  itemId,
29
29
  }: {
30
- /** null turns editorial checks off. */
30
+ /** null when there is no backend to check against (a preview). Whether a publication runs checks is its stored setting. */
31
31
  backend: EditorialChecksBackend | null;
32
32
  body: string | null;
33
33
  section: string;
34
34
  itemId: string | null;
35
35
  }) {
36
- const enabled = backend !== null;
36
+ const available = backend !== null;
37
37
  const [settings, setSettings] = useState<EditorialSettings>(DEFAULT_EDITORIAL_SETTINGS);
38
+ const [settingsLoaded, setSettingsLoaded] = useState(false);
39
+ const enabled = available && settings.enabled;
38
40
  const [result, setResult] = useState<EditorialCheckResult | null>(null);
39
41
  const [checkedBody, setCheckedBody] = useState<string | null>(null);
40
42
  const [status, setStatus] = useState<CheckStatus>({ state: "idle" });
@@ -46,9 +48,9 @@ export function useEditorialChecks({
46
48
  const sequence = useRef(0);
47
49
 
48
50
  useEffect(() => {
49
- if (!enabled) return;
50
- backend?.loadSettings().then(({ settings: loaded }) => setSettings(loaded)).catch(() => undefined);
51
- }, [backend, enabled]);
51
+ if (!available) return;
52
+ backend?.loadSettings().then(({ settings: loaded }) => setSettings(loaded)).catch(() => undefined).finally(() => setSettingsLoaded(true));
53
+ }, [available, backend]);
52
54
 
53
55
  useEffect(() => {
54
56
  if (!enabled || !itemId) return;
@@ -57,7 +59,7 @@ export function useEditorialChecks({
57
59
 
58
60
  const check = useCallback(async () => {
59
61
  const text = latestBody.current;
60
- if (!backend || text === null || !text.trim()) return;
62
+ if (!backend || !enabled || text === null || !text.trim()) return;
61
63
  const run = ++sequence.current;
62
64
  setStatus({ state: "checking" });
63
65
  try {
@@ -73,13 +75,13 @@ export function useEditorialChecks({
73
75
  } catch (error) {
74
76
  if (run === sequence.current) setStatus({ state: "error", message: error instanceof Error ? error.message : "The check failed." });
75
77
  }
76
- }, [backend, section]);
78
+ }, [backend, enabled, section]);
77
79
 
78
80
  useEffect(() => {
79
- if (!enabled || !settings.checkWhileTyping || body === null || body === checkedBody) return;
81
+ if (!enabled || !settingsLoaded || !settings.checkWhileTyping || body === null || body === checkedBody) return;
80
82
  const timer = window.setTimeout(() => void check(), CHECK_PAUSE_MS);
81
83
  return () => window.clearTimeout(timer);
82
- }, [body, check, checkedBody, enabled, settings.checkWhileTyping]);
84
+ }, [body, check, checkedBody, enabled, settings.checkWhileTyping, settingsLoaded]);
83
85
 
84
86
  const record = useCallback(
85
87
  (finding: EditorialFinding, decision: FeedbackDecision, reason: string | null = null) => {
@@ -105,7 +107,7 @@ export function useEditorialChecks({
105
107
  [backend, section],
106
108
  );
107
109
 
108
- const current = result && checkedBody === body;
110
+ const current = enabled && result && checkedBody === body;
109
111
  return {
110
112
  enabled,
111
113
  settings,
@@ -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);
@@ -1,7 +1,7 @@
1
1
  import type { EditorialCheckResult, EditorialFeedbackInput, EditorialFinding, RewriteResult, Span } from "./editorial-checks";
2
2
  import type { EditorialSettings } from "./editorial-checks-settings";
3
3
 
4
- /** What the article editor needs from editorial checks: the plugin's GraphQL API, or the demo. */
4
+ /** What the article editor needs from editorial checks: the site's GraphQL API, or the demo. */
5
5
  export type EditorialChecksBackend = {
6
6
  loadSettings: () => Promise<{ settings: EditorialSettings; exists: boolean }>;
7
7
  dismissedFindingIds: (itemId: string) => Promise<Set<string>>;
@@ -184,7 +184,7 @@ export async function recalibrate(): Promise<{ updated: number; detectors: { pol
184
184
  return parsed(data.recalibrateEditorialPolicies);
185
185
  }
186
186
 
187
- /** The plugin's GraphQL API, signed in as the editor. */
187
+ /** The site's GraphQL API, signed in as the editor. */
188
188
  export const graphqlEditorialBackend: EditorialChecksBackend = {
189
189
  loadSettings,
190
190
  dismissedFindingIds,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Demo-mode editorial checks: no backend, a few stock phrases, canned rewrites.
3
- * The shapes match Limatus results so the editor, the preview marks and the
4
- * rewrite flow behave as they do against the real plugin.
3
+ * The shapes match Humanus results so the editor, the preview marks and the
4
+ * rewrite flow behave as they do against the real backend.
5
5
  */
6
6
  import type { EditorialCheckResult, EditorialFinding, EditorialSegment, RewriteResult, Span } from "./editorial-checks";
7
7
  import type { EditorialChecksBackend } from "./editorial-checks-backend";
@@ -1,5 +1,7 @@
1
1
  /** Runtime settings for editorial checks, stored in EditorialCheckSettings (id "settings"). */
2
2
  export type EditorialSettings = {
3
+ /** Core to every site and on by default; a publication turns its checks off here. */
4
+ enabled: boolean;
3
5
  lanes: { rules: boolean; classifier: boolean };
4
6
  classifierModel: string;
5
7
  rewriteModel: string;
@@ -12,6 +14,7 @@ export type EditorialSettings = {
12
14
  };
13
15
 
14
16
  export const DEFAULT_EDITORIAL_SETTINGS: EditorialSettings = {
17
+ enabled: true,
15
18
  lanes: { rules: true, classifier: true },
16
19
  classifierModel: "jev-latest",
17
20
  rewriteModel: "gpt-6-luna",
@@ -1,11 +1,8 @@
1
1
  /**
2
- * Editorial checks: the Limatus result shapes the newsroom uses, and pure helpers
3
- * over them. Limatus counts offsets in UTF-16 code units, which is exactly how
2
+ * Editorial checks: the Humanus result shapes the newsroom uses, and pure helpers
3
+ * over them. Humanus counts offsets in UTF-16 code units, which is exactly how
4
4
  * JavaScript indexes strings, so spans apply to the body text as they are.
5
5
  */
6
- import { pluginSettings, type PluginOutputs } from "./plugin-settings";
7
-
8
- export const EDITORIAL_CHECKS_PLUGIN = "editorial-checks";
9
6
 
10
7
  export type Span = { start: number; end: number };
11
8
  export type Severity = "info" | "warn" | "error";
@@ -91,10 +88,6 @@ export const UNSAVED_ITEM_ID = "unsaved";
91
88
 
92
89
  const SEVERITY_ORDER: Record<Severity, number> = { error: 0, warn: 1, info: 2 };
93
90
 
94
- export function editorialChecksEnabled(outputs: PluginOutputs | null | undefined): boolean {
95
- return pluginSettings(outputs, EDITORIAL_CHECKS_PLUGIN) !== null;
96
- }
97
-
98
91
  /** Findings worth showing: not dismissed, not covering the whole document, most severe first, then in reading order. */
99
92
  export function visibleFindings(findings: EditorialFinding[], dismissed: ReadonlySet<string>): EditorialFinding[] {
100
93
  return findings
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Places Limatus findings on the rendered Markus preview.
2
+ * Places Humanus findings on the rendered Markus preview.
3
3
  *
4
- * Limatus reports spans in the markup; the preview renders an IR tree with no
4
+ * Humanus reports spans in the markup; the preview renders an IR tree with no
5
5
  * character offsets. Both sides agree on the words a reader sees, so findings
6
- * are placed by text: each Limatus paragraph is matched, in document order, to
6
+ * are placed by text: each Humanus paragraph is matched, in document order, to
7
7
  * the rendered prose block with the same words, and each finding's plain
8
8
  * excerpt is located inside that block. Whitespace is ignored while matching,
9
- * because rendering and Limatus collapse it differently (citations, breaks).
9
+ * because rendering and Humanus collapse it differently (citations, breaks).
10
10
  *
11
11
  * "Rendered text" is defined by `inlineLeaves`, which the preview renderer also
12
12
  * uses, so offsets computed here are offsets the renderer can split on.
@@ -131,7 +131,7 @@ function blockKey(text: string): string {
131
131
  return compact(text).value.replace(/[*_`~]/g, "");
132
132
  }
133
133
 
134
- /** Match every Limatus paragraph to a rendered block and place its findings and sentences there. */
134
+ /** Match every Humanus paragraph to a rendered block and place its findings and sentences there. */
135
135
  export function planPreview(
136
136
  document: MarkusDocument,
137
137
  result: EditorialCheckResult,
@@ -9,14 +9,19 @@ export function resolveBrandConfigPath(relativePath: string): string {
9
9
  return path.isAbsolute(relativePath) ? relativePath : path.join(process.cwd(), relativePath);
10
10
  }
11
11
 
12
- export function getBrandSteeringConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string {
13
- return resolveBrandConfigPath(brand.steeringConfigPath);
12
+ /** Optional repo files a brand may still name; undefined means the site keeps that configuration as backend data and nothing is looked up. */
13
+ function optionalBrandConfigPath(relativePath: string | undefined): string | undefined {
14
+ return relativePath ? resolveBrandConfigPath(relativePath) : undefined;
14
15
  }
15
16
 
16
- export function getBrandNewsroomSectionsConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string {
17
- return resolveBrandConfigPath(brand.newsroomSectionsConfigPath);
17
+ export function getBrandSteeringConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
18
+ return optionalBrandConfigPath(brand.steeringConfigPath);
18
19
  }
19
20
 
20
- export function getBrandAnalysisProfilesPath(brand: SiteBrand = resolveActiveSiteBrand()): string {
21
- return resolveBrandConfigPath(brand.analysisProfilesPath);
21
+ export function getBrandNewsroomSectionsConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
22
+ return optionalBrandConfigPath(brand.newsroomSectionsConfigPath);
23
+ }
24
+
25
+ export function getBrandAnalysisProfilesPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
26
+ return optionalBrandConfigPath(brand.analysisProfilesPath);
22
27
  }
package/lib/site-brand.ts CHANGED
@@ -142,9 +142,12 @@ export type SiteBrand = {
142
142
  readerBasePath?: string;
143
143
  opsChrome: OpsChrome;
144
144
  corpusKey: string;
145
- steeringConfigPath: string;
146
- newsroomSectionsConfigPath: string;
147
- analysisProfilesPath: string;
145
+ /** Optional repo file for topic steering. Editorial and steering configuration is backend data; a data-driven site sets none and nothing is looked up. */
146
+ steeringConfigPath?: string;
147
+ /** Optional repo file for the local-preview demo sections only. Sections live in the backend. */
148
+ newsroomSectionsConfigPath?: string;
149
+ /** Optional repo file listing analysis profiles. */
150
+ analysisProfilesPath?: string;
148
151
  publicationName: string;
149
152
  components?: SiteBrandComponents;
150
153
  /**
package/package.json CHANGED
@@ -395,7 +395,7 @@
395
395
  "optional": true
396
396
  }
397
397
  },
398
- "version": "3.3.0",
398
+ "version": "5.0.0",
399
399
  "dependencies": {
400
400
  "@aws-amplify/adapter-nextjs": "^1.7.3",
401
401
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.3.0",
2
+ "version": "5.0.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",
@@ -1 +0,0 @@
1
- limatus[jev]>=0.31,<0.32
@@ -1,71 +0,0 @@
1
- import { secret } from "@aws-amplify/backend";
2
- import { Stack } from "aws-cdk-lib";
3
- import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
4
- import { editorialChecks } from "./function/resource.js";
5
- import { editorialChecksSchemaDefinitions } from "./schema.js";
6
- export const EDITORIAL_CHECKS_QUERIES = ["checkEditorial", "suggestEditorialRewrite", "draftEditorialQuestions", "tryEditorialPolicy"];
7
- export const EDITORIAL_CHECKS_MUTATIONS = ["importEditorialStarterPack", "recalibrateEditorialPolicies"];
8
- /** The model operations the function performs with its own role. */
9
- export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
10
- "Query/fields/listKnowledgeCorporaByRoleAndName",
11
- "Query/fields/getEditorialContext",
12
- "Query/fields/listEditorialPolicies",
13
- "Query/fields/getEditorialCheckSettings",
14
- "Query/fields/listEditorialFeedbacks",
15
- "Mutation/fields/createEditorialPolicy",
16
- "Mutation/fields/updateEditorialPolicy",
17
- ];
18
- export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
19
- /**
20
- * SSM parameters holding the provider keys. A Python function reads its secret()
21
- * values itself (Amplify only resolves them for Node functions), so it needs read
22
- * access to the app's branch and shared secret paths, and to the sandbox paths.
23
- */
24
- export function editorialChecksSecretParameters(region, account, appId) {
25
- const arn = (path) => `arn:aws:ssm:${region}:${account}:parameter/amplify/${path}`;
26
- return EDITORIAL_CHECKS_SECRETS.flatMap((name) => [
27
- arn(`papyrus/*/${name}`),
28
- arn(`shared/papyrus/${name}`),
29
- ...(appId ? [arn(`${appId}/*/${name}`), arn(`shared/${appId}/${name}`)] : []),
30
- ]);
31
- }
32
- export const editorialChecksManifest = {
33
- name: "editorial-checks",
34
- contract: 1,
35
- models: ["EditorialPolicy", "EditorialCheckSettings", "EditorialFeedback"],
36
- mutations: [...EDITORIAL_CHECKS_QUERIES, ...EDITORIAL_CHECKS_MUTATIONS],
37
- guest: { mutations: [] },
38
- newsroomPanels: [{ id: "editorial-checks", label: "Editorial checks" }],
39
- staticAssets: [],
40
- };
41
- export function resolveEditorialChecksConfig(request) {
42
- const raw = typeof request === "string" ? { name: request } : request;
43
- const unknown = Object.keys(raw).filter((key) => key !== "name");
44
- if (unknown.length > 0)
45
- throw new Error(`Unknown editorial-checks plugin settings: ${unknown.join(", ")} (runtime settings are edited in the newsroom)`);
46
- return {};
47
- }
48
- export const editorialChecksPlugin = {
49
- manifest: editorialChecksManifest,
50
- resolveConfig: resolveEditorialChecksConfig,
51
- functions: () => ({ editorialChecks }),
52
- schemaDefinitions: editorialChecksSchemaDefinitions,
53
- publicSettings: () => ({ contract: editorialChecksManifest.contract }),
54
- wire(backend, _config, context) {
55
- const lambda = backend.editorialChecks.resources.lambda;
56
- const stack = Stack.of(lambda);
57
- lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", context.graphqlEndpoint);
58
- for (const name of EDITORIAL_CHECKS_SECRETS)
59
- backend.editorialChecks.addEnvironment(name, secret(name));
60
- lambda.addToRolePolicy(new PolicyStatement({
61
- effect: Effect.ALLOW,
62
- actions: ["ssm:GetParameter", "ssm:GetParameters"],
63
- resources: editorialChecksSecretParameters(stack.region, stack.account, (process.env.AWS_APP_ID ?? "").trim()),
64
- }));
65
- lambda.addToRolePolicy(new PolicyStatement({
66
- effect: Effect.ALLOW,
67
- actions: ["appsync:GraphQL"],
68
- resources: EDITORIAL_CHECKS_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${context.graphqlApiId}/types/${field}`),
69
- }));
70
- },
71
- };
@@ -1,87 +0,0 @@
1
- import { secret } from "@aws-amplify/backend";
2
- import { Stack } from "aws-cdk-lib";
3
- import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
4
- import type { Function as LambdaFunction } from "aws-cdk-lib/aws-lambda";
5
- import type { PapyrusPlugin, PluginManifest, PluginRequest, PluginWiringContext } from "../types";
6
- import { editorialChecks } from "./function/resource";
7
- import { editorialChecksSchemaDefinitions } from "./schema";
8
-
9
- /** Runtime settings (lanes, models, deadline) live in EditorialCheckSettings, edited in the newsroom; enabling the plugin takes no options. */
10
- export type EditorialChecksPluginConfig = Record<string, never>;
11
-
12
- export const EDITORIAL_CHECKS_QUERIES = ["checkEditorial", "suggestEditorialRewrite", "draftEditorialQuestions", "tryEditorialPolicy"];
13
- export const EDITORIAL_CHECKS_MUTATIONS = ["importEditorialStarterPack", "recalibrateEditorialPolicies"];
14
-
15
- /** The model operations the function performs with its own role. */
16
- export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
17
- "Query/fields/listKnowledgeCorporaByRoleAndName",
18
- "Query/fields/getEditorialContext",
19
- "Query/fields/listEditorialPolicies",
20
- "Query/fields/getEditorialCheckSettings",
21
- "Query/fields/listEditorialFeedbacks",
22
- "Mutation/fields/createEditorialPolicy",
23
- "Mutation/fields/updateEditorialPolicy",
24
- ];
25
-
26
- export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
27
-
28
- /**
29
- * SSM parameters holding the provider keys. A Python function reads its secret()
30
- * values itself (Amplify only resolves them for Node functions), so it needs read
31
- * access to the app's branch and shared secret paths, and to the sandbox paths.
32
- */
33
- export function editorialChecksSecretParameters(region: string, account: string, appId: string): string[] {
34
- const arn = (path: string) => `arn:aws:ssm:${region}:${account}:parameter/amplify/${path}`;
35
- return EDITORIAL_CHECKS_SECRETS.flatMap((name) => [
36
- arn(`papyrus/*/${name}`),
37
- arn(`shared/papyrus/${name}`),
38
- ...(appId ? [arn(`${appId}/*/${name}`), arn(`shared/${appId}/${name}`)] : []),
39
- ]);
40
- }
41
-
42
- export const editorialChecksManifest: PluginManifest = {
43
- name: "editorial-checks",
44
- contract: 1,
45
- models: ["EditorialPolicy", "EditorialCheckSettings", "EditorialFeedback"],
46
- mutations: [...EDITORIAL_CHECKS_QUERIES, ...EDITORIAL_CHECKS_MUTATIONS],
47
- guest: { mutations: [] },
48
- newsroomPanels: [{ id: "editorial-checks", label: "Editorial checks" }],
49
- staticAssets: [],
50
- };
51
-
52
- export function resolveEditorialChecksConfig(request: PluginRequest): EditorialChecksPluginConfig {
53
- const raw = typeof request === "string" ? { name: request } : request;
54
- const unknown = Object.keys(raw).filter((key) => key !== "name");
55
- if (unknown.length > 0) throw new Error(`Unknown editorial-checks plugin settings: ${unknown.join(", ")} (runtime settings are edited in the newsroom)`);
56
- return {};
57
- }
58
-
59
- export const editorialChecksPlugin: PapyrusPlugin<EditorialChecksPluginConfig, { editorialChecks: typeof editorialChecks }> = {
60
- manifest: editorialChecksManifest,
61
- resolveConfig: resolveEditorialChecksConfig,
62
- functions: () => ({ editorialChecks }),
63
- schemaDefinitions: editorialChecksSchemaDefinitions,
64
- publicSettings: () => ({ contract: editorialChecksManifest.contract }),
65
- wire(backend, _config, context: PluginWiringContext) {
66
- const lambda = backend.editorialChecks.resources.lambda as LambdaFunction;
67
- const stack = Stack.of(lambda);
68
- lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", context.graphqlEndpoint);
69
- for (const name of EDITORIAL_CHECKS_SECRETS) backend.editorialChecks.addEnvironment(name, secret(name));
70
- lambda.addToRolePolicy(
71
- new PolicyStatement({
72
- effect: Effect.ALLOW,
73
- actions: ["ssm:GetParameter", "ssm:GetParameters"],
74
- resources: editorialChecksSecretParameters(stack.region, stack.account, (process.env.AWS_APP_ID ?? "").trim()),
75
- }),
76
- );
77
- lambda.addToRolePolicy(
78
- new PolicyStatement({
79
- effect: Effect.ALLOW,
80
- actions: ["appsync:GraphQL"],
81
- resources: EDITORIAL_CHECKS_FUNCTION_FIELDS.map(
82
- (field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${context.graphqlApiId}/types/${field}`,
83
- ),
84
- }),
85
- );
86
- },
87
- };