@anthusai/papyrus 5.8.1 → 5.9.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.
@@ -24,6 +24,7 @@ from papyrus_content.rebuild_trigger import (
24
24
  )
25
25
  from papyrus_content.story_reporting import save_story_version
26
26
  from papyrus_content.publication import ensure_publication_from_environment
27
+ from papyrus_content.editorial_checks import GraphQLEditorialStore, ensure_default_policies
27
28
  from papyrus_content.reference_intake import IntakeError, intake_references
28
29
  from papyrus_content.publishing import (
29
30
  ItemFields,
@@ -150,9 +151,19 @@ def schedule_unpurged_invalidation(keys: list[str]) -> None:
150
151
  schedule_projection_task({"task": "invalidate", "keys": keys}, "edge invalidation failed")
151
152
 
152
153
 
154
+ def ensure_default_editorial_policies(client) -> None:
155
+ """A deployed site starts with working editorial policies. Never fails the deploy: a problem is logged."""
156
+ try:
157
+ result = ensure_default_policies(GraphQLEditorialStore(execute=client.graphql))
158
+ print(json.dumps({"defaultEditorialPolicies": result}))
159
+ except Exception as error:
160
+ print(json.dumps({"defaultEditorialPoliciesError": str(error)[:500]}))
161
+
162
+
153
163
  def run_deploy_render_task(client) -> dict:
154
164
  """Render every published page after a backend deploy, then report drift (a dry run: nothing is deleted)."""
155
165
  ensure_publication_from_environment(client)
166
+ ensure_default_editorial_policies(client)
156
167
  context = projection_context(client)
157
168
  report = deploy_render(context)
158
169
  print(drift_metric_line(report))
@@ -3,3 +3,4 @@ citeproc-py>=0.8.2,<1
3
3
  anthus-markus==0.7.0
4
4
  Pillow>=11.3,<12
5
5
  awscrt>=0.23,<1
6
+ humanus>=0.33,<0.34
@@ -47,6 +47,35 @@ export const editorialChecksSchemaDefinitions = {
47
47
  .disableOperations(["subscriptions"])
48
48
  .secondaryIndexes((index) => [index("itemId").queryField("listEditorialFeedbackByItem")])
49
49
  .authorization((allow) => [allow.group("editor").to(["create", "read"]), allow.group("admin")]),
50
+ EditorialCheckRun: a
51
+ .model({
52
+ id: a.id().required(),
53
+ itemId: a.string().required(),
54
+ lineageId: a.string().required(),
55
+ versionNumber: a.integer().required(),
56
+ section: a.string(),
57
+ contentHash: a.string(),
58
+ packVersion: a.string(),
59
+ violations: a.integer().required(),
60
+ reviews: a.integer().required(),
61
+ findings: a.json().required(),
62
+ lanes: a.json(),
63
+ checkedAt: a.datetime().required(),
64
+ })
65
+ .disableOperations(["subscriptions"])
66
+ .secondaryIndexes((index) => [index("lineageId").sortKeys(["versionNumber"]).queryField("listEditorialCheckRunsByLineage")])
67
+ .authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
68
+ listEditorialStarterPacks: a
69
+ .query()
70
+ .returns(a.json())
71
+ .authorization((allow) => [allow.groups(editors)])
72
+ .handler(a.handler.function(editorialChecks)),
73
+ checkItemVersion: a
74
+ .mutation()
75
+ .arguments({ itemId: a.id().required() })
76
+ .returns(a.json())
77
+ .authorization((allow) => [allow.groups(editors)])
78
+ .handler(a.handler.function(editorialChecks)),
50
79
  checkEditorial: a
51
80
  .query()
52
81
  .arguments({ body: a.string().required(), format: a.string(), section: a.string() })
@@ -49,6 +49,35 @@ export const editorialChecksSchemaDefinitions = {
49
49
  .disableOperations(["subscriptions"])
50
50
  .secondaryIndexes((index) => [index("itemId").queryField("listEditorialFeedbackByItem")])
51
51
  .authorization((allow) => [allow.group("editor").to(["create", "read"]), allow.group("admin")]),
52
+ EditorialCheckRun: a
53
+ .model({
54
+ id: a.id().required(),
55
+ itemId: a.string().required(),
56
+ lineageId: a.string().required(),
57
+ versionNumber: a.integer().required(),
58
+ section: a.string(),
59
+ contentHash: a.string(),
60
+ packVersion: a.string(),
61
+ violations: a.integer().required(),
62
+ reviews: a.integer().required(),
63
+ findings: a.json().required(),
64
+ lanes: a.json(),
65
+ checkedAt: a.datetime().required(),
66
+ })
67
+ .disableOperations(["subscriptions"])
68
+ .secondaryIndexes((index) => [index("lineageId").sortKeys(["versionNumber"]).queryField("listEditorialCheckRunsByLineage")])
69
+ .authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
70
+ listEditorialStarterPacks: a
71
+ .query()
72
+ .returns(a.json())
73
+ .authorization((allow) => [allow.groups(editors)])
74
+ .handler(a.handler.function(editorialChecks)),
75
+ checkItemVersion: a
76
+ .mutation()
77
+ .arguments({ itemId: a.id().required() })
78
+ .returns(a.json())
79
+ .authorization((allow) => [allow.groups(editors)])
80
+ .handler(a.handler.function(editorialChecks)),
52
81
  checkEditorial: a
53
82
  .query()
54
83
  .arguments({ body: a.string().required(), format: a.string(), section: a.string() })
@@ -10,6 +10,10 @@ export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
10
10
  "Query/fields/listEditorialFeedbacks",
11
11
  "Mutation/fields/createEditorialPolicy",
12
12
  "Mutation/fields/updateEditorialPolicy",
13
+ "Query/fields/getItem",
14
+ "Query/fields/getEditorialCheckRun",
15
+ "Mutation/fields/createEditorialCheckRun",
16
+ "Mutation/fields/updateEditorialCheckRun",
13
17
  ];
14
18
  export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
15
19
  /**
@@ -12,6 +12,10 @@ export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
12
12
  "Query/fields/listEditorialFeedbacks",
13
13
  "Mutation/fields/createEditorialPolicy",
14
14
  "Mutation/fields/updateEditorialPolicy",
15
+ "Query/fields/getItem",
16
+ "Query/fields/getEditorialCheckRun",
17
+ "Mutation/fields/createEditorialCheckRun",
18
+ "Mutation/fields/updateEditorialCheckRun",
15
19
  ];
16
20
 
17
21
  export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
@@ -579,6 +579,10 @@ export function defineSiteBackend(papyrusSite) {
579
579
  backend.addOutput({
580
580
  custom: {
581
581
  publicForms: { endpoint: publicFormsEndpoint },
582
+ // Lets the operator CLI re-project on publish without PAPYRUS_PUBLICATION_KIND / PAPYRUS_CONTENT_ACTIONS_FUNCTION (PPY-0c0b75).
583
+ publication: s3ProjectionPublication !== undefined
584
+ ? { kind: "s3-projection", contentActionsFunction: contentActionsLambda.functionName }
585
+ : null,
582
586
  prospectusSignIn: prospectusGoogleOnlyClient
583
587
  ? { clientId: prospectusGoogleOnlyClient.ref, callbackPath: PROSPECTUS_SIGN_IN_CALLBACK_PATH }
584
588
  : null,
@@ -723,6 +723,11 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
723
723
  backend.addOutput({
724
724
  custom: {
725
725
  publicForms: { endpoint: publicFormsEndpoint },
726
+ // Lets the operator CLI re-project on publish without PAPYRUS_PUBLICATION_KIND / PAPYRUS_CONTENT_ACTIONS_FUNCTION (PPY-0c0b75).
727
+ publication:
728
+ s3ProjectionPublication !== undefined
729
+ ? { kind: "s3-projection", contentActionsFunction: contentActionsLambda.functionName }
730
+ : null,
726
731
  prospectusSignIn: prospectusGoogleOnlyClient
727
732
  ? { clientId: prospectusGoogleOnlyClient.ref, callbackPath: PROSPECTUS_SIGN_IN_CALLBACK_PATH }
728
733
  : null,
@@ -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,13 +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_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;
29
26
  const POLICY_KEY = /^[a-z0-9][a-z0-9._-]*$/;
30
27
 
31
28
  type Draft = {
@@ -91,7 +88,8 @@ export function EditorialChecksAdminPanel() {
91
88
  const [settings, setSettings] = useState<EditorialSettings>(DEFAULT_EDITORIAL_SETTINGS);
92
89
  const [settingsExist, setSettingsExist] = useState(false);
93
90
  const [draft, setDraft] = useState<Draft | null>(null);
94
- const [starterPack, setStarterPack] = useState<string>(STARTER_PACKS[0].name);
91
+ const [starterPacks, setStarterPacks] = useState<StarterPackSummary[]>([]);
92
+ const [starterPack, setStarterPack] = useState<string>("");
95
93
  const [message, setMessage] = useState<string | null>(null);
96
94
  const [working, setWorking] = useState<string | null>(null);
97
95
  const [tryText, setTryText] = useState("");
@@ -99,7 +97,9 @@ export function EditorialChecksAdminPanel() {
99
97
 
100
98
  const refresh = useCallback(async () => {
101
99
  try {
102
- 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 || "");
103
103
  setPolicies(loadedPolicies);
104
104
  setSettings(loadedSettings.settings);
105
105
  setSettingsExist(loadedSettings.exists);
@@ -163,14 +163,18 @@ export function EditorialChecksAdminPanel() {
163
163
  <div className="flex flex-wrap gap-2">
164
164
  <Button onClick={() => { setDraft(draftFrom(emptyPolicy(), true)); setTryResult(null); }} size="sm" type="button">New policy</Button>
165
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>)}
166
+ {starterPacks.map((pack) => (
167
+ <option key={pack.name} value={pack.name}>
168
+ {pack.title} ({pack.importedCount} of {pack.policyCount} added)
169
+ </option>
170
+ ))}
167
171
  </select>
168
172
  <Button
169
- disabled={working !== null}
173
+ disabled={working !== null || !starterPack}
170
174
  onClick={() => run("import", async () => {
171
175
  const outcome = await importStarterPack(starterPack);
172
176
  await refresh();
173
- const label = STARTER_PACKS.find((pack) => pack.name === starterPack)?.label ?? starterPack;
177
+ const label = starterPacks.find((pack) => pack.name === starterPack)?.title ?? starterPack;
174
178
  return `${label}: ${outcome.created} policies added, ${outcome.skipped} already present.`;
175
179
  })}
176
180
  size="sm"
@@ -179,6 +183,17 @@ export function EditorialChecksAdminPanel() {
179
183
  >
180
184
  Add starter pack
181
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">
182
197
  <Button
183
198
  disabled={working !== null}
184
199
  onClick={() => run("recalibrate", async () => {
@@ -161,6 +161,24 @@ export async function importStarterPack(name: string): Promise<{ created: number
161
161
  return parsed(data.importEditorialStarterPack);
162
162
  }
163
163
 
164
+ export type StarterPackSummary = {
165
+ name: string;
166
+ title: string;
167
+ version?: string | null;
168
+ language?: string | null;
169
+ guidance?: string | null;
170
+ policyCount: number;
171
+ importedCount: number;
172
+ isDefault: boolean;
173
+ policies: { id: string; title?: string | null; severity?: string | null; tags: string[]; lanes: string[] }[];
174
+ };
175
+
176
+ /** The starter packs the site's Humanus ships, with how many of each pack's policies the site already has. */
177
+ export async function listStarterPacks(): Promise<{ humanusVersion?: string | null; packs: StarterPackSummary[] }> {
178
+ const data = await call<{ listEditorialStarterPacks: unknown }>(`query ListStarterPacks { listEditorialStarterPacks }`);
179
+ return parsed(data.listEditorialStarterPacks);
180
+ }
181
+
164
182
  export async function tryPolicy(policy: Record<string, unknown>, text: string): Promise<EditorialCheckResult> {
165
183
  const data = await call<{ tryEditorialPolicy: unknown }>(
166
184
  `query TryEditorialPolicy($policy: AWSJSON!, $text: String!) { tryEditorialPolicy(policy: $policy, text: $text, format: "markus") }`,
package/package.json CHANGED
@@ -406,7 +406,7 @@
406
406
  "optional": true
407
407
  }
408
408
  },
409
- "version": "5.8.1",
409
+ "version": "5.9.0",
410
410
  "dependencies": {
411
411
  "@aws-amplify/adapter-nextjs": "^1.7.3",
412
412
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.8.1",
2
+ "version": "5.9.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",