@anthusai/papyrus 5.30.0 → 5.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/resource.js +19 -0
- package/amplify/data/resource.ts +20 -0
- package/amplify/data/schema.js +21 -0
- package/amplify/data/schema.ts +22 -0
- package/amplify/functions/automation-view/handler.py +11 -0
- package/amplify/functions/automation-view/resource.js +24 -0
- package/amplify/functions/automation-view/resource.ts +34 -0
- package/amplify/functions/automation-view/schema.js +16 -0
- package/amplify/functions/automation-view/schema.ts +18 -0
- package/amplify/functions/automation-view/wire.js +32 -0
- package/amplify/functions/automation-view/wire.ts +46 -0
- package/amplify/plugins/reader-steering/config.js +8 -1
- package/amplify/plugins/reader-steering/config.ts +13 -1
- package/amplify/plugins/reader-steering/function/handler.py +209 -18
- package/amplify/plugins/reader-steering/function/proposals.py +255 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +10 -2
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +10 -2
- package/amplify/plugins/reader-steering/schema.js +65 -1
- package/amplify/plugins/reader-steering/schema.ts +65 -1
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/components/automation-panel.tsx +39 -0
- package/components/automation-view.tsx +149 -0
- package/components/topic-steering-workspace.tsx +13 -2
- package/lib/automation-client.ts +29 -0
- package/lib/automation-fixture.ts +65 -0
- package/lib/automation-overview.ts +138 -0
- package/lib/reader-api.ts +1 -1
- package/package.json +7 -1
- package/routes.manifest.json +12 -1
package/amplify/data/resource.js
CHANGED
|
@@ -1163,6 +1163,25 @@ const schema = a.schema({
|
|
|
1163
1163
|
allow.group("editor").to(["read"]),
|
|
1164
1164
|
allow.group(adminGroup).to(authoringOperations),
|
|
1165
1165
|
]),
|
|
1166
|
+
Automation: a
|
|
1167
|
+
.model({
|
|
1168
|
+
id: a.id().required(),
|
|
1169
|
+
title: a.string().required(),
|
|
1170
|
+
description: a.string(),
|
|
1171
|
+
enabled: a.boolean().required(),
|
|
1172
|
+
trigger: a.json().required(),
|
|
1173
|
+
procedureKey: a.string().required(),
|
|
1174
|
+
inputTemplate: a.json(),
|
|
1175
|
+
limits: a.json(),
|
|
1176
|
+
createdBy: a.string(),
|
|
1177
|
+
createdAt: a.datetime().required(),
|
|
1178
|
+
updatedBy: a.string(),
|
|
1179
|
+
updatedAt: a.datetime().required(),
|
|
1180
|
+
})
|
|
1181
|
+
.authorization((allow) => [
|
|
1182
|
+
allow.group("editor").to(["read", "create", "update"]),
|
|
1183
|
+
allow.group(adminGroup).to(authoringOperations),
|
|
1184
|
+
]),
|
|
1166
1185
|
NewsroomSection: a
|
|
1167
1186
|
.model({
|
|
1168
1187
|
id: a.id().required(),
|
package/amplify/data/resource.ts
CHANGED
|
@@ -1259,6 +1259,26 @@ const schema = a.schema({
|
|
|
1259
1259
|
allow.group(adminGroup).to(authoringOperations),
|
|
1260
1260
|
]),
|
|
1261
1261
|
|
|
1262
|
+
Automation: a
|
|
1263
|
+
.model({
|
|
1264
|
+
id: a.id().required(),
|
|
1265
|
+
title: a.string().required(),
|
|
1266
|
+
description: a.string(),
|
|
1267
|
+
enabled: a.boolean().required(),
|
|
1268
|
+
trigger: a.json().required(),
|
|
1269
|
+
procedureKey: a.string().required(),
|
|
1270
|
+
inputTemplate: a.json(),
|
|
1271
|
+
limits: a.json(),
|
|
1272
|
+
createdBy: a.string(),
|
|
1273
|
+
createdAt: a.datetime().required(),
|
|
1274
|
+
updatedBy: a.string(),
|
|
1275
|
+
updatedAt: a.datetime().required(),
|
|
1276
|
+
})
|
|
1277
|
+
.authorization((allow) => [
|
|
1278
|
+
allow.group("editor").to(["read", "create", "update"]),
|
|
1279
|
+
allow.group(adminGroup).to(authoringOperations),
|
|
1280
|
+
]),
|
|
1281
|
+
|
|
1262
1282
|
NewsroomSection: a
|
|
1263
1283
|
.model({
|
|
1264
1284
|
id: a.id().required(),
|
package/amplify/data/schema.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
|
|
5
|
+
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema.js";
|
|
5
6
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
|
|
6
7
|
import { contentActions } from "../functions/content-actions/resource.js";
|
|
7
8
|
import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
|
|
@@ -1082,6 +1083,25 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1082
1083
|
allow.group("editor").to(["read"]),
|
|
1083
1084
|
allow.group(adminGroup).to(authoringOperations),
|
|
1084
1085
|
]),
|
|
1086
|
+
/** A named "when X happens, run procedure Y" (PPY-9ff95b). Step 1 mirrors the monitoring router's procedureTriggers and starts nothing itself. */
|
|
1087
|
+
Automation: modelWithoutSubscriptions({
|
|
1088
|
+
id: a.id().required(),
|
|
1089
|
+
title: a.string().required(),
|
|
1090
|
+
description: a.string(),
|
|
1091
|
+
enabled: a.boolean().required(),
|
|
1092
|
+
trigger: a.json().required(),
|
|
1093
|
+
procedureKey: a.string().required(),
|
|
1094
|
+
inputTemplate: a.json(),
|
|
1095
|
+
limits: a.json(),
|
|
1096
|
+
createdBy: a.string(),
|
|
1097
|
+
createdAt: a.datetime().required(),
|
|
1098
|
+
updatedBy: a.string(),
|
|
1099
|
+
updatedAt: a.datetime().required(),
|
|
1100
|
+
})
|
|
1101
|
+
.authorization((allow) => [
|
|
1102
|
+
allow.group("editor").to(["read", "create", "update"]),
|
|
1103
|
+
allow.group(adminGroup).to(authoringOperations),
|
|
1104
|
+
]),
|
|
1085
1105
|
NewsroomSection: modelWithoutSubscriptions({
|
|
1086
1106
|
id: a.id().required(),
|
|
1087
1107
|
tenantId: a.string(),
|
|
@@ -2021,6 +2041,7 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
2021
2041
|
]),
|
|
2022
2042
|
...editorialChecksSchemaDefinitions,
|
|
2023
2043
|
...monitoringViewSchemaDefinitions,
|
|
2044
|
+
...automationViewSchemaDefinitions,
|
|
2024
2045
|
};
|
|
2025
2046
|
if (!includeEditions) {
|
|
2026
2047
|
const mutableDefinitions = definitions;
|
package/amplify/data/schema.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
5
|
+
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema";
|
|
5
6
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema";
|
|
6
7
|
import { contentActions } from "../functions/content-actions/resource";
|
|
7
8
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
@@ -1176,6 +1177,26 @@ const definitions = {
|
|
|
1176
1177
|
allow.group(adminGroup).to(authoringOperations),
|
|
1177
1178
|
]),
|
|
1178
1179
|
|
|
1180
|
+
/** A named "when X happens, run procedure Y" (PPY-9ff95b). Step 1 mirrors the monitoring router's procedureTriggers and starts nothing itself. */
|
|
1181
|
+
Automation: modelWithoutSubscriptions({
|
|
1182
|
+
id: a.id().required(),
|
|
1183
|
+
title: a.string().required(),
|
|
1184
|
+
description: a.string(),
|
|
1185
|
+
enabled: a.boolean().required(),
|
|
1186
|
+
trigger: a.json().required(),
|
|
1187
|
+
procedureKey: a.string().required(),
|
|
1188
|
+
inputTemplate: a.json(),
|
|
1189
|
+
limits: a.json(),
|
|
1190
|
+
createdBy: a.string(),
|
|
1191
|
+
createdAt: a.datetime().required(),
|
|
1192
|
+
updatedBy: a.string(),
|
|
1193
|
+
updatedAt: a.datetime().required(),
|
|
1194
|
+
})
|
|
1195
|
+
.authorization((allow) => [
|
|
1196
|
+
allow.group("editor").to(["read", "create", "update"]),
|
|
1197
|
+
allow.group(adminGroup).to(authoringOperations),
|
|
1198
|
+
]),
|
|
1199
|
+
|
|
1179
1200
|
NewsroomSection: modelWithoutSubscriptions({
|
|
1180
1201
|
id: a.id().required(),
|
|
1181
1202
|
tenantId: a.string(),
|
|
@@ -2145,6 +2166,7 @@ const definitions = {
|
|
|
2145
2166
|
]),
|
|
2146
2167
|
...editorialChecksSchemaDefinitions,
|
|
2147
2168
|
...monitoringViewSchemaDefinitions,
|
|
2169
|
+
...automationViewSchemaDefinitions,
|
|
2148
2170
|
};
|
|
2149
2171
|
if (!includeEditions) {
|
|
2150
2172
|
const mutableDefinitions = definitions as Record<string, unknown>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The automation-view function: `automationOverview` for editors and admins, read-only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from papyrus_content.automation_view import handle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
|
|
11
|
+
return handle(event)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineFunction } from "@aws-amplify/backend";
|
|
2
|
+
import { Duration } from "aws-cdk-lib";
|
|
3
|
+
import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
5
|
+
/** AppSync gives a resolver 30 seconds; a table read and ten run lookups need a few. */
|
|
6
|
+
export const AUTOMATION_VIEW_TIMEOUT_SECONDS = 25;
|
|
7
|
+
/** stdlib plus the runtime's boto3: the bundle is `papyrus_content` and nothing else. */
|
|
8
|
+
export const automationView = defineFunction((scope) => new Function(scope, "papyrus-automation-view", {
|
|
9
|
+
runtime: Runtime.PYTHON_3_12,
|
|
10
|
+
architecture: Architecture.ARM_64,
|
|
11
|
+
handler: "handler.handler",
|
|
12
|
+
timeout: Duration.seconds(AUTOMATION_VIEW_TIMEOUT_SECONDS),
|
|
13
|
+
memorySize: 256,
|
|
14
|
+
code: pythonLambdaCode({ functionDir: "automation-view", modules: ["papyrus_content"], noDeps: true }, [
|
|
15
|
+
"bash",
|
|
16
|
+
"-c",
|
|
17
|
+
[
|
|
18
|
+
"set -euo pipefail",
|
|
19
|
+
"mkdir -p /asset-output/papyrus_content",
|
|
20
|
+
"cp amplify/functions/automation-view/handler.py /asset-output/handler.py",
|
|
21
|
+
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
22
|
+
].join(" && "),
|
|
23
|
+
]),
|
|
24
|
+
}), { resourceGroupName: "data" });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineFunction } from "@aws-amplify/backend";
|
|
2
|
+
import { Duration } from "aws-cdk-lib";
|
|
3
|
+
import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
import type { Construct } from "constructs";
|
|
5
|
+
import { pythonLambdaCode } from "../shared/python-bundle";
|
|
6
|
+
|
|
7
|
+
/** AppSync gives a resolver 30 seconds; a table read and ten run lookups need a few. */
|
|
8
|
+
export const AUTOMATION_VIEW_TIMEOUT_SECONDS = 25;
|
|
9
|
+
|
|
10
|
+
/** stdlib plus the runtime's boto3: the bundle is `papyrus_content` and nothing else. */
|
|
11
|
+
export const automationView = defineFunction(
|
|
12
|
+
(scope: Construct) =>
|
|
13
|
+
new Function(scope, "papyrus-automation-view", {
|
|
14
|
+
runtime: Runtime.PYTHON_3_12,
|
|
15
|
+
architecture: Architecture.ARM_64,
|
|
16
|
+
handler: "handler.handler",
|
|
17
|
+
timeout: Duration.seconds(AUTOMATION_VIEW_TIMEOUT_SECONDS),
|
|
18
|
+
memorySize: 256,
|
|
19
|
+
code: pythonLambdaCode(
|
|
20
|
+
{ functionDir: "automation-view", modules: ["papyrus_content"], noDeps: true },
|
|
21
|
+
[
|
|
22
|
+
"bash",
|
|
23
|
+
"-c",
|
|
24
|
+
[
|
|
25
|
+
"set -euo pipefail",
|
|
26
|
+
"mkdir -p /asset-output/papyrus_content",
|
|
27
|
+
"cp amplify/functions/automation-view/handler.py /asset-output/handler.py",
|
|
28
|
+
"cp -R src/papyrus_content/. /asset-output/papyrus_content/",
|
|
29
|
+
].join(" && "),
|
|
30
|
+
],
|
|
31
|
+
),
|
|
32
|
+
}),
|
|
33
|
+
{ resourceGroupName: "data" },
|
|
34
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { automationView } from "./resource.js";
|
|
3
|
+
const editors = ["editor", "admin"];
|
|
4
|
+
/**
|
|
5
|
+
* Core to every site's data API. The Automation page reads the automation records, the monitoring
|
|
6
|
+
* trigger rows they link to, and the procedure runs through this one query. AppSync admits editors
|
|
7
|
+
* and admins only; the function checks the groups again.
|
|
8
|
+
*/
|
|
9
|
+
export const automationViewSchemaDefinitions = {
|
|
10
|
+
automationOverview: a
|
|
11
|
+
.query()
|
|
12
|
+
.arguments({ automationId: a.string() })
|
|
13
|
+
.returns(a.json())
|
|
14
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
15
|
+
.handler(a.handler.function(automationView)),
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { automationView } from "./resource";
|
|
3
|
+
|
|
4
|
+
const editors = ["editor", "admin"];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Core to every site's data API. The Automation page reads the automation records, the monitoring
|
|
8
|
+
* trigger rows they link to, and the procedure runs through this one query. AppSync admits editors
|
|
9
|
+
* and admins only; the function checks the groups again.
|
|
10
|
+
*/
|
|
11
|
+
export const automationViewSchemaDefinitions = {
|
|
12
|
+
automationOverview: a
|
|
13
|
+
.query()
|
|
14
|
+
.arguments({ automationId: a.string() })
|
|
15
|
+
.returns(a.json())
|
|
16
|
+
.authorization((allow) => [allow.groups(editors)])
|
|
17
|
+
.handler(a.handler.function(automationView)),
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import { MONITORING_TABLE_PARAMETER, MONITORING_VIEW_TABLE_ACTIONS, monitoringViewParameterArn, monitoringViewTableArns, } from "../monitoring-view/wire.js";
|
|
4
|
+
/** The two reads the function makes of its own site's API with its own role. It writes nothing. */
|
|
5
|
+
export const AUTOMATION_VIEW_FUNCTION_FIELDS = ["Query/fields/listAutomations", "Query/fields/getProcedureRun"];
|
|
6
|
+
/**
|
|
7
|
+
* Environment and least-privilege IAM for the automation-view function: two fields of the site's own API,
|
|
8
|
+
* and the same read-only grant on the monitoring table that the Monitoring page has. A site outside the
|
|
9
|
+
* monitoring account finds no parameter and the page lists its records without runs.
|
|
10
|
+
*/
|
|
11
|
+
export function wireAutomationView(backend) {
|
|
12
|
+
const lambda = backend.automationView.resources.lambda;
|
|
13
|
+
const stack = Stack.of(lambda);
|
|
14
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
15
|
+
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
16
|
+
const apiId = backend.data.resources.graphqlApi.apiId;
|
|
17
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
18
|
+
effect: Effect.ALLOW,
|
|
19
|
+
actions: ["appsync:GraphQL"],
|
|
20
|
+
resources: AUTOMATION_VIEW_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
|
|
21
|
+
}));
|
|
22
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
23
|
+
effect: Effect.ALLOW,
|
|
24
|
+
actions: ["ssm:GetParameter"],
|
|
25
|
+
resources: [monitoringViewParameterArn(stack.region, stack.account)],
|
|
26
|
+
}));
|
|
27
|
+
lambda.addToRolePolicy(new PolicyStatement({
|
|
28
|
+
effect: Effect.ALLOW,
|
|
29
|
+
actions: MONITORING_VIEW_TABLE_ACTIONS,
|
|
30
|
+
resources: monitoringViewTableArns(stack.region, stack.account),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import type { Function as LambdaFunction } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
import {
|
|
5
|
+
MONITORING_TABLE_PARAMETER,
|
|
6
|
+
MONITORING_VIEW_TABLE_ACTIONS,
|
|
7
|
+
monitoringViewParameterArn,
|
|
8
|
+
monitoringViewTableArns,
|
|
9
|
+
} from "../monitoring-view/wire";
|
|
10
|
+
|
|
11
|
+
/** The two reads the function makes of its own site's API with its own role. It writes nothing. */
|
|
12
|
+
export const AUTOMATION_VIEW_FUNCTION_FIELDS = ["Query/fields/listAutomations", "Query/fields/getProcedureRun"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Environment and least-privilege IAM for the automation-view function: two fields of the site's own API,
|
|
16
|
+
* and the same read-only grant on the monitoring table that the Monitoring page has. A site outside the
|
|
17
|
+
* monitoring account finds no parameter and the page lists its records without runs.
|
|
18
|
+
*/
|
|
19
|
+
export function wireAutomationView(backend: any): void {
|
|
20
|
+
const lambda = backend.automationView.resources.lambda as LambdaFunction;
|
|
21
|
+
const stack = Stack.of(lambda);
|
|
22
|
+
lambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl as string);
|
|
23
|
+
lambda.addEnvironment("PAPYRUS_MONITORING_TABLE_PARAMETER", MONITORING_TABLE_PARAMETER);
|
|
24
|
+
const apiId = backend.data.resources.graphqlApi.apiId as string;
|
|
25
|
+
lambda.addToRolePolicy(
|
|
26
|
+
new PolicyStatement({
|
|
27
|
+
effect: Effect.ALLOW,
|
|
28
|
+
actions: ["appsync:GraphQL"],
|
|
29
|
+
resources: AUTOMATION_VIEW_FUNCTION_FIELDS.map((field) => `arn:aws:appsync:${stack.region}:${stack.account}:apis/${apiId}/types/${field}`),
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
lambda.addToRolePolicy(
|
|
33
|
+
new PolicyStatement({
|
|
34
|
+
effect: Effect.ALLOW,
|
|
35
|
+
actions: ["ssm:GetParameter"],
|
|
36
|
+
resources: [monitoringViewParameterArn(stack.region, stack.account)],
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
lambda.addToRolePolicy(
|
|
40
|
+
new PolicyStatement({
|
|
41
|
+
effect: Effect.ALLOW,
|
|
42
|
+
actions: MONITORING_VIEW_TABLE_ACTIONS,
|
|
43
|
+
resources: monitoringViewTableArns(stack.region, stack.account),
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -5,8 +5,9 @@ export const MODERATION_MODES = ["off", "human", "ai"];
|
|
|
5
5
|
export const AI_KEY_PARAMETER = /^\/papyrus\/[a-z0-9][a-z0-9-]{0,62}\/openai-api-key$/;
|
|
6
6
|
export const AI_MODEL_NAME = /^[a-z0-9][a-z0-9.-]{0,63}$/;
|
|
7
7
|
export const SUBMISSION_KIND_NAME = /^[a-z][a-z0-9-]{0,23}$/;
|
|
8
|
+
export const SUBJECT_TYPES = ["item", "submission", "story", "proposal", "concept"];
|
|
8
9
|
export const COMPUTE_ROLE_ARN = /^arn:aws:iam::[0-9]{12}:role\/[A-Za-z0-9+=,.@_/-]{1,512}$/;
|
|
9
|
-
const KNOWN_KEYS = new Set(["name", "computeRoleArn", "dimensions", "limits", "reservedHandles", "karma", "ranking", "snapshot", "retirementPoll", "reservedConcurrency", "hypeGap", "submissions", "comments", "moderation"]);
|
|
10
|
+
const KNOWN_KEYS = new Set(["name", "computeRoleArn", "dimensions", "limits", "reservedHandles", "karma", "ranking", "snapshot", "retirementPoll", "reservedConcurrency", "hypeGap", "submissions", "comments", "moderation", "subjects"]);
|
|
10
11
|
function object(raw, label, known) {
|
|
11
12
|
if (raw === undefined)
|
|
12
13
|
return {};
|
|
@@ -111,6 +112,11 @@ export function resolveReaderSteeringConfig(request) {
|
|
|
111
112
|
throw new Error(`reader-steering plugin moderation.ai.model must be a model name matching ${AI_MODEL_NAME}`);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
115
|
+
const subjects = object(raw.subjects, "subjects", ["types"]);
|
|
116
|
+
const subjectTypes = subjects.types === undefined ? ["item", "submission"] : subjects.types;
|
|
117
|
+
if (!Array.isArray(subjectTypes) || subjectTypes.length === 0 || !subjectTypes.every((type) => SUBJECT_TYPES.includes(type))) {
|
|
118
|
+
throw new Error(`reader-steering plugin subjects.types must be a non-empty list of: ${SUBJECT_TYPES.join(", ")}`);
|
|
119
|
+
}
|
|
114
120
|
const kinds = names(submissions.kinds, "submissions.kinds", SUBMISSION_KIND_NAME);
|
|
115
121
|
const comments = object(raw.comments, "comments", ["perHour", "editWindowMinutes", "maximumLength", "maximumLinks"]);
|
|
116
122
|
const hypeGap = resolveHypeGap(raw.hypeGap, dimensions);
|
|
@@ -154,6 +160,7 @@ export function resolveReaderSteeringConfig(request) {
|
|
|
154
160
|
itemType: lowercaseName(submissions.itemType, "submissions.itemType", "brief"),
|
|
155
161
|
section: lowercaseName(submissions.section, "submissions.section", "unsectioned"),
|
|
156
162
|
},
|
|
163
|
+
subjects: { types: [...new Set(subjectTypes)] },
|
|
157
164
|
moderation: {
|
|
158
165
|
mode: mode,
|
|
159
166
|
preModeration,
|
|
@@ -7,6 +7,7 @@ export const MODERATION_MODES = ["off", "human", "ai"] as const;
|
|
|
7
7
|
export const AI_KEY_PARAMETER = /^\/papyrus\/[a-z0-9][a-z0-9-]{0,62}\/openai-api-key$/;
|
|
8
8
|
export const AI_MODEL_NAME = /^[a-z0-9][a-z0-9.-]{0,63}$/;
|
|
9
9
|
export const SUBMISSION_KIND_NAME = /^[a-z][a-z0-9-]{0,23}$/;
|
|
10
|
+
export const SUBJECT_TYPES = ["item", "submission", "story", "proposal", "concept"] as const;
|
|
10
11
|
export const COMPUTE_ROLE_ARN = /^arn:aws:iam::[0-9]{12}:role\/[A-Za-z0-9+=,.@_/-]{1,512}$/;
|
|
11
12
|
|
|
12
13
|
export type ReaderSteeringConfig = {
|
|
@@ -63,6 +64,11 @@ export type ReaderSteeringConfig = {
|
|
|
63
64
|
/** The section of that draft. */
|
|
64
65
|
section: string;
|
|
65
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* The kinds of subject a signed-in reader may vote on and flag, by `{type, id}`. Default: items and submissions, which is
|
|
69
|
+
* what readers could vote on before subject types existed. `story`, `proposal` and `concept` are opt-in.
|
|
70
|
+
*/
|
|
71
|
+
subjects: { types: (typeof SUBJECT_TYPES)[number][] };
|
|
66
72
|
moderation: {
|
|
67
73
|
/**
|
|
68
74
|
* `human`: new submissions and comments are public at once and wait in the Administration queue (the default for a pilot).
|
|
@@ -109,7 +115,7 @@ export type ReaderSteeringConfig = {
|
|
|
109
115
|
retirementPoll: boolean;
|
|
110
116
|
};
|
|
111
117
|
|
|
112
|
-
const KNOWN_KEYS = new Set(["name", "computeRoleArn", "dimensions", "limits", "reservedHandles", "karma", "ranking", "snapshot", "retirementPoll", "reservedConcurrency", "hypeGap", "submissions", "comments", "moderation"]);
|
|
118
|
+
const KNOWN_KEYS = new Set(["name", "computeRoleArn", "dimensions", "limits", "reservedHandles", "karma", "ranking", "snapshot", "retirementPoll", "reservedConcurrency", "hypeGap", "submissions", "comments", "moderation", "subjects"]);
|
|
113
119
|
|
|
114
120
|
function object(raw: unknown, label: string, known: string[]): Record<string, unknown> {
|
|
115
121
|
if (raw === undefined) return {};
|
|
@@ -206,6 +212,11 @@ export function resolveReaderSteeringConfig(request: PluginRequest): ReaderSteer
|
|
|
206
212
|
throw new Error(`reader-steering plugin moderation.ai.model must be a model name matching ${AI_MODEL_NAME}`);
|
|
207
213
|
}
|
|
208
214
|
}
|
|
215
|
+
const subjects = object(raw.subjects, "subjects", ["types"]);
|
|
216
|
+
const subjectTypes = subjects.types === undefined ? ["item", "submission"] : subjects.types;
|
|
217
|
+
if (!Array.isArray(subjectTypes) || subjectTypes.length === 0 || !subjectTypes.every((type) => (SUBJECT_TYPES as readonly unknown[]).includes(type))) {
|
|
218
|
+
throw new Error(`reader-steering plugin subjects.types must be a non-empty list of: ${SUBJECT_TYPES.join(", ")}`);
|
|
219
|
+
}
|
|
209
220
|
const kinds = names(submissions.kinds, "submissions.kinds", SUBMISSION_KIND_NAME);
|
|
210
221
|
const comments = object(raw.comments, "comments", ["perHour", "editWindowMinutes", "maximumLength", "maximumLinks"]);
|
|
211
222
|
const hypeGap = resolveHypeGap(raw.hypeGap, dimensions);
|
|
@@ -249,6 +260,7 @@ export function resolveReaderSteeringConfig(request: PluginRequest): ReaderSteer
|
|
|
249
260
|
itemType: lowercaseName(submissions.itemType, "submissions.itemType", "brief"),
|
|
250
261
|
section: lowercaseName(submissions.section, "submissions.section", "unsectioned"),
|
|
251
262
|
},
|
|
263
|
+
subjects: { types: [...new Set(subjectTypes)] as (typeof SUBJECT_TYPES)[number][] },
|
|
252
264
|
moderation: {
|
|
253
265
|
mode: mode as (typeof MODERATION_MODES)[number],
|
|
254
266
|
preModeration,
|