@anthusai/papyrus 3.3.0 → 4.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.
- package/amplify/data/schema.js +2 -0
- package/amplify/data/schema.ts +2 -0
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +1 -1
- package/amplify/functions/editorial-checks/requirements.txt +1 -0
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +5 -5
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +5 -5
- package/amplify/functions/editorial-checks/schema.js +92 -0
- package/amplify/{plugins → functions}/editorial-checks/schema.ts +3 -5
- package/amplify/functions/editorial-checks/wire.js +49 -0
- package/amplify/functions/editorial-checks/wire.ts +57 -0
- package/amplify/plugins/registry.js +0 -2
- package/amplify/plugins/registry.ts +0 -2
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/components/editorial-checks-admin-panel.tsx +14 -4
- package/components/newsroom-article-editor.tsx +2 -4
- package/components/topic-steering-workspace.tsx +10 -17
- package/components/use-editorial-checks.ts +12 -10
- package/lib/editorial-checks-backend.ts +1 -1
- package/lib/editorial-checks-client.ts +1 -1
- package/lib/editorial-checks-demo.ts +1 -1
- package/lib/editorial-checks-settings.ts +3 -0
- package/lib/editorial-checks.ts +0 -7
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
- package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
- package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
- package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
- package/amplify/plugins/editorial-checks/schema.js +0 -94
package/amplify/data/schema.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { a } from "@aws-amplify/backend";
|
|
2
2
|
import { assignmentAction } from "../functions/assignment-action/resource.js";
|
|
3
3
|
import { categoryAction } from "../functions/category-action/resource.js";
|
|
4
|
+
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
|
|
4
5
|
import { contentActions } from "../functions/content-actions/resource.js";
|
|
5
6
|
import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
|
|
6
7
|
import { manageUserRole } from "../functions/manage-user-role/resource.js";
|
|
@@ -1993,6 +1994,7 @@ const baseDefinitions = {
|
|
|
1993
1994
|
allow.guest().to(["read"]),
|
|
1994
1995
|
allow.groups(contentWriteGroups),
|
|
1995
1996
|
]),
|
|
1997
|
+
...editorialChecksSchemaDefinitions,
|
|
1996
1998
|
};
|
|
1997
1999
|
export const schema = a.schema(baseDefinitions).authorization((allow) => [
|
|
1998
2000
|
allow.resource(assignmentAction).to(["query", "mutate"]),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ClientSchema, a } from "@aws-amplify/backend";
|
|
2
2
|
import { assignmentAction } from "../functions/assignment-action/resource";
|
|
3
3
|
import { categoryAction } from "../functions/category-action/resource";
|
|
4
|
+
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
4
5
|
import { contentActions } from "../functions/content-actions/resource";
|
|
5
6
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
6
7
|
import { manageUserRole } from "../functions/manage-user-role/resource";
|
|
@@ -2112,6 +2113,7 @@ const baseDefinitions = {
|
|
|
2112
2113
|
allow.guest().to(["read"]),
|
|
2113
2114
|
allow.groups(contentWriteGroups),
|
|
2114
2115
|
]),
|
|
2116
|
+
...editorialChecksSchemaDefinitions,
|
|
2115
2117
|
};
|
|
2116
2118
|
|
|
2117
2119
|
export const schema = a.schema(baseDefinitions).authorization((allow) => [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
limatus[jev]>=0.32,<0.33
|
package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js
RENAMED
|
@@ -4,12 +4,12 @@ import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { pythonLambdaCode } from "
|
|
7
|
+
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
/** AppSync gives a resolver 30 seconds; Limatus's classifier deadline (settings, default 20 s) keeps a check inside this. */
|
|
10
10
|
export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
|
|
11
11
|
/** Limatus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
|
|
12
|
-
export const editorialChecksRequirements = ["limatus[jev]>=0.
|
|
12
|
+
export const editorialChecksRequirements = ["limatus[jev]>=0.32,<0.33"];
|
|
13
13
|
function installRequirements(outputDir) {
|
|
14
14
|
execFileSync("python3", [
|
|
15
15
|
"-m", "pip", "install", "-r", path.join(functionDirectory, "requirements.txt"), "-t", outputDir, "--no-cache-dir",
|
|
@@ -24,7 +24,7 @@ export const editorialChecks = defineFunction((scope) => new Function(scope, "pa
|
|
|
24
24
|
timeout: Duration.seconds(EDITORIAL_CHECKS_TIMEOUT_SECONDS),
|
|
25
25
|
memorySize: 1024,
|
|
26
26
|
code: pythonLambdaCode({
|
|
27
|
-
functionDir: "
|
|
27
|
+
functionDir: "editorial-checks",
|
|
28
28
|
modules: ["papyrus_content"],
|
|
29
29
|
requirements: editorialChecksRequirements,
|
|
30
30
|
noDeps: true,
|
|
@@ -35,9 +35,9 @@ export const editorialChecks = defineFunction((scope) => new Function(scope, "pa
|
|
|
35
35
|
[
|
|
36
36
|
"set -euo pipefail",
|
|
37
37
|
"mkdir -p /asset-output/papyrus_content",
|
|
38
|
-
"cp amplify/
|
|
38
|
+
"cp amplify/functions/editorial-checks/handler.py /asset-output/handler.py",
|
|
39
39
|
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
40
|
-
"python3 -m pip install -r amplify/
|
|
40
|
+
"python3 -m pip install -r amplify/functions/editorial-checks/requirements.txt -t /asset-output --no-cache-dir",
|
|
41
41
|
].join(" && "),
|
|
42
42
|
]),
|
|
43
43
|
}), { resourceGroupName: "data" });
|
package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts
RENAMED
|
@@ -5,7 +5,7 @@ import type { Construct } from "constructs";
|
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { pythonLambdaCode } from "
|
|
8
|
+
import { pythonLambdaCode } from "../shared/python-bundle";
|
|
9
9
|
|
|
10
10
|
const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
|
13
13
|
export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
|
|
14
14
|
|
|
15
15
|
/** Limatus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
|
|
16
|
-
export const editorialChecksRequirements = ["limatus[jev]>=0.
|
|
16
|
+
export const editorialChecksRequirements = ["limatus[jev]>=0.32,<0.33"];
|
|
17
17
|
|
|
18
18
|
function installRequirements(outputDir: string): void {
|
|
19
19
|
execFileSync(
|
|
@@ -37,7 +37,7 @@ export const editorialChecks = defineFunction(
|
|
|
37
37
|
memorySize: 1024,
|
|
38
38
|
code: pythonLambdaCode(
|
|
39
39
|
{
|
|
40
|
-
functionDir: "
|
|
40
|
+
functionDir: "editorial-checks",
|
|
41
41
|
modules: ["papyrus_content"],
|
|
42
42
|
requirements: editorialChecksRequirements,
|
|
43
43
|
noDeps: true,
|
|
@@ -49,9 +49,9 @@ export const editorialChecks = defineFunction(
|
|
|
49
49
|
[
|
|
50
50
|
"set -euo pipefail",
|
|
51
51
|
"mkdir -p /asset-output/papyrus_content",
|
|
52
|
-
"cp amplify/
|
|
52
|
+
"cp amplify/functions/editorial-checks/handler.py /asset-output/handler.py",
|
|
53
53
|
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
54
|
-
"python3 -m pip install -r amplify/
|
|
54
|
+
"python3 -m pip install -r amplify/functions/editorial-checks/requirements.txt -t /asset-output --no-cache-dir",
|
|
55
55
|
].join(" && "),
|
|
56
56
|
],
|
|
57
57
|
),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { editorialChecks } from "./resource.js";
|
|
3
|
+
const editors = ["editor", "admin"];
|
|
4
|
+
/**
|
|
5
|
+
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus; Limatus checks
|
|
6
|
+
* content against them. Editors read policies and record decisions; admins
|
|
7
|
+
* manage policies and settings. Only the function (IAM) calls Limatus.
|
|
8
|
+
*/
|
|
9
|
+
export const editorialChecksSchemaDefinitions = {
|
|
10
|
+
EditorialPolicy: a
|
|
11
|
+
.model({
|
|
12
|
+
id: a.id().required(),
|
|
13
|
+
policyKey: a.string().required(),
|
|
14
|
+
scope: a.string().required(),
|
|
15
|
+
version: a.integer().required(),
|
|
16
|
+
title: a.string().required(),
|
|
17
|
+
guidance: a.string().required(),
|
|
18
|
+
severity: a.string().required(),
|
|
19
|
+
enabled: a.boolean().required(),
|
|
20
|
+
detectors: a.json().required(),
|
|
21
|
+
examples: a.json(),
|
|
22
|
+
rewriteGuidance: a.string(),
|
|
23
|
+
source: a.string().required(),
|
|
24
|
+
})
|
|
25
|
+
.disableOperations(["subscriptions"])
|
|
26
|
+
.authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
|
|
27
|
+
EditorialCheckSettings: a
|
|
28
|
+
.model({
|
|
29
|
+
id: a.id().required(),
|
|
30
|
+
settings: a.json().required(),
|
|
31
|
+
})
|
|
32
|
+
.disableOperations(["subscriptions"])
|
|
33
|
+
.authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
|
|
34
|
+
EditorialFeedback: a
|
|
35
|
+
.model({
|
|
36
|
+
id: a.id().required(),
|
|
37
|
+
itemId: a.string().required(),
|
|
38
|
+
findingId: a.string().required(),
|
|
39
|
+
policyId: a.string().required(),
|
|
40
|
+
detectorId: a.string().required(),
|
|
41
|
+
lane: a.string().required(),
|
|
42
|
+
probability: a.float(),
|
|
43
|
+
decision: a.string().required(),
|
|
44
|
+
reason: a.string(),
|
|
45
|
+
text: a.string(),
|
|
46
|
+
})
|
|
47
|
+
.disableOperations(["subscriptions"])
|
|
48
|
+
.secondaryIndexes((index) => [index("itemId").queryField("listEditorialFeedbackByItem")])
|
|
49
|
+
.authorization((allow) => [allow.group("editor").to(["create", "read"]), allow.group("admin")]),
|
|
50
|
+
checkEditorial: a
|
|
51
|
+
.query()
|
|
52
|
+
.arguments({ body: a.string().required(), format: a.string(), section: a.string() })
|
|
53
|
+
.returns(a.json())
|
|
54
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
55
|
+
.handler(a.handler.function(editorialChecks)),
|
|
56
|
+
suggestEditorialRewrite: a
|
|
57
|
+
.query()
|
|
58
|
+
.arguments({
|
|
59
|
+
body: a.string().required(),
|
|
60
|
+
format: a.string(),
|
|
61
|
+
section: a.string(),
|
|
62
|
+
finding: a.json(),
|
|
63
|
+
selection: a.json(),
|
|
64
|
+
note: a.string(),
|
|
65
|
+
})
|
|
66
|
+
.returns(a.json())
|
|
67
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
68
|
+
.handler(a.handler.function(editorialChecks)),
|
|
69
|
+
draftEditorialQuestions: a
|
|
70
|
+
.query()
|
|
71
|
+
.arguments({ title: a.string().required(), guidance: a.string().required(), examples: a.json() })
|
|
72
|
+
.returns(a.json())
|
|
73
|
+
.authorization((allow) => [allow.group("admin")])
|
|
74
|
+
.handler(a.handler.function(editorialChecks)),
|
|
75
|
+
tryEditorialPolicy: a
|
|
76
|
+
.query()
|
|
77
|
+
.arguments({ policy: a.json().required(), text: a.string().required(), format: a.string() })
|
|
78
|
+
.returns(a.json())
|
|
79
|
+
.authorization((allow) => [allow.group("admin")])
|
|
80
|
+
.handler(a.handler.function(editorialChecks)),
|
|
81
|
+
importEditorialStarterPack: a
|
|
82
|
+
.mutation()
|
|
83
|
+
.arguments({ name: a.string().required() })
|
|
84
|
+
.returns(a.json())
|
|
85
|
+
.authorization((allow) => [allow.group("admin")])
|
|
86
|
+
.handler(a.handler.function(editorialChecks)),
|
|
87
|
+
recalibrateEditorialPolicies: a
|
|
88
|
+
.mutation()
|
|
89
|
+
.returns(a.json())
|
|
90
|
+
.authorization((allow) => [allow.group("admin")])
|
|
91
|
+
.handler(a.handler.function(editorialChecks)),
|
|
92
|
+
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { a } from "@aws-amplify/backend";
|
|
2
|
-
import { editorialChecks } from "./
|
|
2
|
+
import { editorialChecks } from "./resource";
|
|
3
3
|
|
|
4
4
|
const editors = ["editor", "admin"];
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Policies, settings and editors' decisions live in Papyrus; Limatus checks
|
|
7
|
+
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus; Limatus checks
|
|
8
8
|
* content against them. Editors read policies and record decisions; admins
|
|
9
9
|
* manage policies and settings. Only the function (IAM) calls Limatus.
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
12
|
-
return {
|
|
11
|
+
export const editorialChecksSchemaDefinitions = {
|
|
13
12
|
EditorialPolicy: a
|
|
14
13
|
.model({
|
|
15
14
|
id: a.id().required(),
|
|
@@ -93,4 +92,3 @@ export function editorialChecksSchemaDefinitions() {
|
|
|
93
92
|
.authorization((allow) => [allow.group("admin")])
|
|
94
93
|
.handler(a.handler.function(editorialChecks)),
|
|
95
94
|
};
|
|
96
|
-
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/** The model operations the function performs with its own role. The two context reads are its least-privilege access to private policy. */
|
|
5
|
+
export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
|
|
6
|
+
"Query/fields/listKnowledgeCorporaByRoleAndName",
|
|
7
|
+
"Query/fields/getEditorialContext",
|
|
8
|
+
"Query/fields/listEditorialPolicies",
|
|
9
|
+
"Query/fields/getEditorialCheckSettings",
|
|
10
|
+
"Query/fields/listEditorialFeedbacks",
|
|
11
|
+
"Mutation/fields/createEditorialPolicy",
|
|
12
|
+
"Mutation/fields/updateEditorialPolicy",
|
|
13
|
+
];
|
|
14
|
+
export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
|
|
15
|
+
/**
|
|
16
|
+
* SSM parameters holding the provider keys. A Python function reads its secret()
|
|
17
|
+
* values itself (Amplify only resolves them for Node functions), so it needs read
|
|
18
|
+
* access to the app's branch and shared secret paths, and to the sandbox paths.
|
|
19
|
+
*/
|
|
20
|
+
export function editorialChecksSecretParameters(region, account, appId) {
|
|
21
|
+
const arn = (path) => `arn:aws:ssm:${region}:${account}:parameter/amplify/${path}`;
|
|
22
|
+
return EDITORIAL_CHECKS_SECRETS.flatMap((name) => [
|
|
23
|
+
arn(`papyrus/*/${name}`),
|
|
24
|
+
arn(`shared/papyrus/${name}`),
|
|
25
|
+
...(appId ? [arn(`${appId}/*/${name}`), arn(`shared/${appId}/${name}`)] : []),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Environment, secrets and least-privilege IAM for the editorial-checks function. Every site gets
|
|
30
|
+
* these; whether checks run is a stored setting (EditorialCheckSettings.enabled), not a deploy flag.
|
|
31
|
+
*/
|
|
32
|
+
export function wireEditorialChecks(backend) {
|
|
33
|
+
const lambda = backend.editorialChecks.resources.lambda;
|
|
34
|
+
const stack = Stack.of(lambda);
|
|
35
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
36
|
+
for (const name of EDITORIAL_CHECKS_SECRETS)
|
|
37
|
+
backend.editorialChecks.addEnvironment(name, secret(name));
|
|
38
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
39
|
+
effect: Effect.ALLOW,
|
|
40
|
+
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
41
|
+
resources: editorialChecksSecretParameters(stack.region, stack.account, (process.env.AWS_APP_ID ?? "").trim()),
|
|
42
|
+
}));
|
|
43
|
+
const apiId = backend.data.resources.graphqlApi.apiId;
|
|
44
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
45
|
+
effect: Effect.ALLOW,
|
|
46
|
+
actions: ["appsync:GraphQL"],
|
|
47
|
+
resources: EDITORIAL_CHECKS_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|
|
6
|
+
/** The model operations the function performs with its own role. The two context reads are its least-privilege access to private policy. */
|
|
7
|
+
export const EDITORIAL_CHECKS_FUNCTION_FIELDS = [
|
|
8
|
+
"Query/fields/listKnowledgeCorporaByRoleAndName",
|
|
9
|
+
"Query/fields/getEditorialContext",
|
|
10
|
+
"Query/fields/listEditorialPolicies",
|
|
11
|
+
"Query/fields/getEditorialCheckSettings",
|
|
12
|
+
"Query/fields/listEditorialFeedbacks",
|
|
13
|
+
"Mutation/fields/createEditorialPolicy",
|
|
14
|
+
"Mutation/fields/updateEditorialPolicy",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const EDITORIAL_CHECKS_SECRETS = ["TYPESAFE_API_KEY", "OPENAI_API_KEY"];
|
|
18
|
+
|
|
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: string, account: string, appId: string): string[] {
|
|
25
|
+
const arn = (path: string) => `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
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Environment, secrets and least-privilege IAM for the editorial-checks function. Every site gets
|
|
35
|
+
* these; whether checks run is a stored setting (EditorialCheckSettings.enabled), not a deploy flag.
|
|
36
|
+
*/
|
|
37
|
+
export function wireEditorialChecks(backend: any): void {
|
|
38
|
+
const lambda = backend.editorialChecks.resources.lambda as LambdaFunction;
|
|
39
|
+
const stack = Stack.of(lambda);
|
|
40
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string);
|
|
41
|
+
for (const name of EDITORIAL_CHECKS_SECRETS) backend.editorialChecks.addEnvironment(name, secret(name));
|
|
42
|
+
lambda.addToRolePolicy(
|
|
43
|
+
new PolicyStatement({
|
|
44
|
+
effect: Effect.ALLOW,
|
|
45
|
+
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
46
|
+
resources: editorialChecksSecretParameters(stack.region, stack.account, (process.env.AWS_APP_ID ?? "").trim()),
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const apiId = backend.data.resources.graphqlApi.apiId as string;
|
|
50
|
+
lambda.addToRolePolicy(
|
|
51
|
+
new PolicyStatement({
|
|
52
|
+
effect: Effect.ALLOW,
|
|
53
|
+
actions: ["appsync:GraphQL"],
|
|
54
|
+
resources: EDITORIAL_CHECKS_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { editorialChecksPlugin } from "./editorial-checks/papyrus.plugin.js";
|
|
2
1
|
import { signupPlugin } from "./signup/papyrus.plugin.js";
|
|
3
2
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
4
3
|
export const KNOWN_PLUGINS = {
|
|
5
|
-
"editorial-checks": editorialChecksPlugin,
|
|
6
4
|
signup: signupPlugin,
|
|
7
5
|
};
|
|
8
6
|
export function resolvePlugins(requests) {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { PapyrusPlugin, PluginRequest } from "./types";
|
|
2
|
-
import { editorialChecksPlugin } from "./editorial-checks/papyrus.plugin";
|
|
3
2
|
import { signupPlugin } from "./signup/papyrus.plugin";
|
|
4
3
|
|
|
5
4
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
6
5
|
export const KNOWN_PLUGINS: Record<string, PapyrusPlugin<any, any>> = {
|
|
7
|
-
"editorial-checks": editorialChecksPlugin,
|
|
8
6
|
signup: signupPlugin,
|
|
9
7
|
};
|
|
10
8
|
|
package/amplify/site-backend.js
CHANGED
|
@@ -20,6 +20,8 @@ import { addInvalidationPathAlarm } from "./publication/invalidation-alarm.js";
|
|
|
20
20
|
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource.js";
|
|
21
21
|
import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client.js";
|
|
22
22
|
import { defineSiteData } from "./data/resource.js";
|
|
23
|
+
import { editorialChecks } from "./functions/editorial-checks/resource.js";
|
|
24
|
+
import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
|
|
23
25
|
import { resolvePlugins } from "./plugins/registry.js";
|
|
24
26
|
import { wirePlugins } from "./plugins/wire.js";
|
|
25
27
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
@@ -89,6 +91,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
89
91
|
auth: defineSiteAuth(authConfig),
|
|
90
92
|
categoryAction,
|
|
91
93
|
contentActions,
|
|
94
|
+
editorialChecks,
|
|
92
95
|
data: defineSiteData(pluginDefinitions),
|
|
93
96
|
knowledgeQuery,
|
|
94
97
|
manageUserRole,
|
|
@@ -589,6 +592,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
589
592
|
: null,
|
|
590
593
|
},
|
|
591
594
|
});
|
|
595
|
+
wireEditorialChecks(backend);
|
|
592
596
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
593
597
|
return backend;
|
|
594
598
|
}
|
package/amplify/site-backend.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { addInvalidationPathAlarm } from "./publication/invalidation-alarm";
|
|
|
27
27
|
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource";
|
|
28
28
|
import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client";
|
|
29
29
|
import { defineSiteData } from "./data/resource";
|
|
30
|
+
import { editorialChecks } from "./functions/editorial-checks/resource";
|
|
31
|
+
import { wireEditorialChecks } from "./functions/editorial-checks/wire";
|
|
30
32
|
import { resolvePlugins } from "./plugins/registry";
|
|
31
33
|
import { wirePlugins } from "./plugins/wire";
|
|
32
34
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
@@ -110,6 +112,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
110
112
|
auth: defineSiteAuth(authConfig),
|
|
111
113
|
categoryAction,
|
|
112
114
|
contentActions,
|
|
115
|
+
editorialChecks,
|
|
113
116
|
data: defineSiteData(pluginDefinitions),
|
|
114
117
|
knowledgeQuery,
|
|
115
118
|
manageUserRole,
|
|
@@ -732,6 +735,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
732
735
|
: null,
|
|
733
736
|
},
|
|
734
737
|
});
|
|
738
|
+
wireEditorialChecks(backend);
|
|
735
739
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
736
740
|
|
|
737
741
|
return backend;
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
type EditorialSettings,
|
|
22
22
|
} from "../lib/editorial-checks-client";
|
|
23
23
|
|
|
24
|
-
const
|
|
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 = {
|
|
@@ -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("");
|
|
@@ -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(
|
|
171
|
+
const outcome = await importStarterPack(starterPack);
|
|
164
172
|
await refresh();
|
|
165
|
-
|
|
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
|
|
180
|
+
Add starter pack
|
|
172
181
|
</Button>
|
|
173
182
|
<Button
|
|
174
183
|
disabled={working !== null}
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
>
|
|
4328
|
-
|
|
4329
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
50
|
-
backend?.loadSettings().then(({ settings: loaded }) => setSettings(loaded)).catch(() => undefined);
|
|
51
|
-
}, [
|
|
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,
|
|
@@ -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
|
|
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
|
|
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
3
|
* The shapes match Limatus results so the editor, the preview marks and the
|
|
4
|
-
* rewrite flow behave as they do against the real
|
|
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",
|
package/lib/editorial-checks.ts
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
* over them. Limatus 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
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED
|
@@ -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
|
-
};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { a } from "@aws-amplify/backend";
|
|
2
|
-
import { editorialChecks } from "./function/resource.js";
|
|
3
|
-
const editors = ["editor", "admin"];
|
|
4
|
-
/**
|
|
5
|
-
* Policies, settings and editors' decisions live in Papyrus; Limatus checks
|
|
6
|
-
* content against them. Editors read policies and record decisions; admins
|
|
7
|
-
* manage policies and settings. Only the function (IAM) calls Limatus.
|
|
8
|
-
*/
|
|
9
|
-
export function editorialChecksSchemaDefinitions() {
|
|
10
|
-
return {
|
|
11
|
-
EditorialPolicy: a
|
|
12
|
-
.model({
|
|
13
|
-
id: a.id().required(),
|
|
14
|
-
policyKey: a.string().required(),
|
|
15
|
-
scope: a.string().required(),
|
|
16
|
-
version: a.integer().required(),
|
|
17
|
-
title: a.string().required(),
|
|
18
|
-
guidance: a.string().required(),
|
|
19
|
-
severity: a.string().required(),
|
|
20
|
-
enabled: a.boolean().required(),
|
|
21
|
-
detectors: a.json().required(),
|
|
22
|
-
examples: a.json(),
|
|
23
|
-
rewriteGuidance: a.string(),
|
|
24
|
-
source: a.string().required(),
|
|
25
|
-
})
|
|
26
|
-
.disableOperations(["subscriptions"])
|
|
27
|
-
.authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
|
|
28
|
-
EditorialCheckSettings: a
|
|
29
|
-
.model({
|
|
30
|
-
id: a.id().required(),
|
|
31
|
-
settings: a.json().required(),
|
|
32
|
-
})
|
|
33
|
-
.disableOperations(["subscriptions"])
|
|
34
|
-
.authorization((allow) => [allow.group("editor").to(["read"]), allow.group("admin")]),
|
|
35
|
-
EditorialFeedback: a
|
|
36
|
-
.model({
|
|
37
|
-
id: a.id().required(),
|
|
38
|
-
itemId: a.string().required(),
|
|
39
|
-
findingId: a.string().required(),
|
|
40
|
-
policyId: a.string().required(),
|
|
41
|
-
detectorId: a.string().required(),
|
|
42
|
-
lane: a.string().required(),
|
|
43
|
-
probability: a.float(),
|
|
44
|
-
decision: a.string().required(),
|
|
45
|
-
reason: a.string(),
|
|
46
|
-
text: a.string(),
|
|
47
|
-
})
|
|
48
|
-
.disableOperations(["subscriptions"])
|
|
49
|
-
.secondaryIndexes((index) => [index("itemId").queryField("listEditorialFeedbackByItem")])
|
|
50
|
-
.authorization((allow) => [allow.group("editor").to(["create", "read"]), allow.group("admin")]),
|
|
51
|
-
checkEditorial: a
|
|
52
|
-
.query()
|
|
53
|
-
.arguments({ body: a.string().required(), format: a.string(), section: a.string() })
|
|
54
|
-
.returns(a.json())
|
|
55
|
-
.authorization((allow) => [allow.groups(editors)])
|
|
56
|
-
.handler(a.handler.function(editorialChecks)),
|
|
57
|
-
suggestEditorialRewrite: a
|
|
58
|
-
.query()
|
|
59
|
-
.arguments({
|
|
60
|
-
body: a.string().required(),
|
|
61
|
-
format: a.string(),
|
|
62
|
-
section: a.string(),
|
|
63
|
-
finding: a.json(),
|
|
64
|
-
selection: a.json(),
|
|
65
|
-
note: a.string(),
|
|
66
|
-
})
|
|
67
|
-
.returns(a.json())
|
|
68
|
-
.authorization((allow) => [allow.groups(editors)])
|
|
69
|
-
.handler(a.handler.function(editorialChecks)),
|
|
70
|
-
draftEditorialQuestions: a
|
|
71
|
-
.query()
|
|
72
|
-
.arguments({ title: a.string().required(), guidance: a.string().required(), examples: a.json() })
|
|
73
|
-
.returns(a.json())
|
|
74
|
-
.authorization((allow) => [allow.group("admin")])
|
|
75
|
-
.handler(a.handler.function(editorialChecks)),
|
|
76
|
-
tryEditorialPolicy: a
|
|
77
|
-
.query()
|
|
78
|
-
.arguments({ policy: a.json().required(), text: a.string().required(), format: a.string() })
|
|
79
|
-
.returns(a.json())
|
|
80
|
-
.authorization((allow) => [allow.group("admin")])
|
|
81
|
-
.handler(a.handler.function(editorialChecks)),
|
|
82
|
-
importEditorialStarterPack: a
|
|
83
|
-
.mutation()
|
|
84
|
-
.arguments({ name: a.string().required() })
|
|
85
|
-
.returns(a.json())
|
|
86
|
-
.authorization((allow) => [allow.group("admin")])
|
|
87
|
-
.handler(a.handler.function(editorialChecks)),
|
|
88
|
-
recalibrateEditorialPolicies: a
|
|
89
|
-
.mutation()
|
|
90
|
-
.returns(a.json())
|
|
91
|
-
.authorization((allow) => [allow.group("admin")])
|
|
92
|
-
.handler(a.handler.function(editorialChecks)),
|
|
93
|
-
};
|
|
94
|
-
}
|