@anthusai/papyrus 2.0.0-next.1 → 2.0.0-next.10

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 (68) hide show
  1. package/amplify/data/resource.js +7 -0
  2. package/amplify/data/resource.ts +8 -0
  3. package/amplify/data/schema.js +8 -0
  4. package/amplify/data/schema.ts +9 -0
  5. package/amplify/functions/assignment-action/data-access.js +88 -0
  6. package/amplify/functions/assignment-action/data-access.ts +87 -0
  7. package/amplify/functions/content-actions/handler.py +7 -0
  8. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  9. package/amplify/plugins/editorial-checks/function/handler.py +14 -0
  10. package/amplify/plugins/editorial-checks/function/requirements.txt +1 -0
  11. package/amplify/plugins/editorial-checks/function/resource.js +43 -0
  12. package/amplify/plugins/editorial-checks/function/resource.ts +60 -0
  13. package/amplify/plugins/editorial-checks/papyrus.plugin.js +71 -0
  14. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +87 -0
  15. package/amplify/plugins/editorial-checks/schema.js +94 -0
  16. package/amplify/plugins/editorial-checks/schema.ts +96 -0
  17. package/amplify/plugins/registry.js +2 -0
  18. package/amplify/plugins/registry.ts +2 -0
  19. package/amplify/site-backend.js +5 -0
  20. package/amplify/site-backend.ts +11 -0
  21. package/components/editorial-checks-admin-panel.tsx +337 -0
  22. package/components/editorial-finding-card.tsx +162 -0
  23. package/components/editorial-findings-panel.tsx +91 -0
  24. package/components/editorial-preview-popover.tsx +103 -0
  25. package/components/markus-body-editor.tsx +213 -0
  26. package/components/markus-ir-preview.tsx +147 -6
  27. package/components/newsroom-article-editor.tsx +138 -11
  28. package/components/newsroom-board-view.tsx +6 -7
  29. package/components/newsroom-ops-shell.tsx +7 -3
  30. package/components/newsroom-organizer.css +57 -61
  31. package/components/newsroom-theme-fonts/broadsheet-green.css +10 -0
  32. package/components/newsroom-theme-fonts/broadsheet-green.tsx +6 -0
  33. package/components/newsroom-theme-fonts/newspaper-gothic.css +9 -0
  34. package/components/newsroom-theme-fonts/newspaper-gothic.tsx +6 -0
  35. package/components/newsroom-theme-fonts/night-desk.css +10 -0
  36. package/components/newsroom-theme-fonts/night-desk.tsx +6 -0
  37. package/components/newsroom-theme-fonts/refined-newsprint.css +11 -0
  38. package/components/newsroom-theme-fonts/refined-newsprint.tsx +6 -0
  39. package/components/newsroom-theme.css +39 -0
  40. package/components/newsroom-theme.tsx +33 -0
  41. package/components/reference-cyclotron-priming.tsx +12 -13
  42. package/components/reference-cyclotron-status.tsx +4 -4
  43. package/components/reference-cyclotron-suggestion.tsx +6 -6
  44. package/components/reference-live-agreement.tsx +10 -10
  45. package/components/reference-priming.tsx +1 -1
  46. package/components/reference-relevance-review.tsx +1 -1
  47. package/components/reference-section-suggestion.tsx +1 -1
  48. package/components/topic-steering-workspace.tsx +19 -1
  49. package/components/use-editorial-checks.ts +131 -0
  50. package/lib/cyclotron-suggestions.ts +9 -9
  51. package/lib/editorial-checks-backend.ts +16 -0
  52. package/lib/editorial-checks-client.ts +194 -0
  53. package/lib/editorial-checks-demo.ts +147 -0
  54. package/lib/editorial-checks-settings.ts +24 -0
  55. package/lib/editorial-checks.ts +178 -0
  56. package/lib/editorial-preview.ts +228 -0
  57. package/lib/newsroom-board.ts +27 -2
  58. package/lib/relevance-status.ts +4 -4
  59. package/lib/site-brand.ts +17 -0
  60. package/package.json +34 -2
  61. package/routes.manifest.json +1 -1
  62. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  63. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  64. package/components/newsroom-fonts/playfair-display-OFL.txt +0 -93
  65. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  66. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  67. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +0 -93
  68. package/components/newsroom-newsprint-board.css +0 -7
@@ -589,6 +589,12 @@ const schema = a.schema({
589
589
  .returns(a.json())
590
590
  .authorization((allow) => [allow.groups(contentWriteGroups)])
591
591
  .handler(a.handler.function(contentActions)),
592
+ intakePendingReferences: a
593
+ .mutation()
594
+ .arguments({ input: a.json().required() })
595
+ .returns(a.json())
596
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
597
+ .handler(a.handler.function(contentActions)),
592
598
  saveItemDraft: a
593
599
  .mutation()
594
600
  .arguments({ input: a.json().required() })
@@ -2146,6 +2152,7 @@ const schema = a.schema({
2146
2152
  allow.groups(contentWriteGroups),
2147
2153
  ]),
2148
2154
  }).authorization((allow) => [
2155
+ allow.resource(assignmentAction).to(["query", "mutate"]),
2149
2156
  allow.resource(categoryAction),
2150
2157
  allow.resource(contentActions),
2151
2158
  allow.resource(manageUserRole),
@@ -641,6 +641,13 @@ const schema = a.schema({
641
641
  .authorization((allow) => [allow.groups(contentWriteGroups)])
642
642
  .handler(a.handler.function(contentActions)),
643
643
 
644
+ intakePendingReferences: a
645
+ .mutation()
646
+ .arguments({ input: a.json().required() })
647
+ .returns(a.json())
648
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
649
+ .handler(a.handler.function(contentActions)),
650
+
644
651
  saveItemDraft: a
645
652
  .mutation()
646
653
  .arguments({ input: a.json().required() })
@@ -2271,6 +2278,7 @@ const schema = a.schema({
2271
2278
  allow.groups(contentWriteGroups),
2272
2279
  ]),
2273
2280
  }).authorization((allow) => [
2281
+ allow.resource(assignmentAction).to(["query", "mutate"]),
2274
2282
  allow.resource(categoryAction),
2275
2283
  allow.resource(contentActions),
2276
2284
  allow.resource(manageUserRole),
@@ -496,6 +496,12 @@ const baseDefinitions = {
496
496
  .returns(a.json())
497
497
  .authorization((allow) => [allow.groups(contentWriteGroups)])
498
498
  .handler(a.handler.function(contentActions)),
499
+ intakePendingReferences: a
500
+ .mutation()
501
+ .arguments({ input: a.json().required() })
502
+ .returns(a.json())
503
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
504
+ .handler(a.handler.function(contentActions)),
499
505
  saveItemDraft: a
500
506
  .mutation()
501
507
  .arguments({ input: a.json().required() })
@@ -1989,6 +1995,7 @@ const baseDefinitions = {
1989
1995
  ]),
1990
1996
  };
1991
1997
  export const schema = a.schema(baseDefinitions).authorization((allow) => [
1998
+ allow.resource(assignmentAction).to(["query", "mutate"]),
1992
1999
  allow.resource(categoryAction),
1993
2000
  allow.resource(contentActions),
1994
2001
  allow.resource(manageUserRole),
@@ -1999,6 +2006,7 @@ export const schema = a.schema(baseDefinitions).authorization((allow) => [
1999
2006
  /** The deployed schema of a site that enables plugins: the base definitions plus what each plugin contributes (docs/plugins.md). */
2000
2007
  export function schemaWithPlugins(pluginDefinitions) {
2001
2008
  return a.schema({ ...baseDefinitions, ...pluginDefinitions }).authorization((allow) => [
2009
+ allow.resource(assignmentAction).to(["query", "mutate"]),
2002
2010
  allow.resource(categoryAction),
2003
2011
  allow.resource(contentActions),
2004
2012
  allow.resource(manageUserRole),
@@ -542,6 +542,13 @@ const baseDefinitions = {
542
542
  .authorization((allow) => [allow.groups(contentWriteGroups)])
543
543
  .handler(a.handler.function(contentActions)),
544
544
 
545
+ intakePendingReferences: a
546
+ .mutation()
547
+ .arguments({ input: a.json().required() })
548
+ .returns(a.json())
549
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
550
+ .handler(a.handler.function(contentActions)),
551
+
545
552
  saveItemDraft: a
546
553
  .mutation()
547
554
  .arguments({ input: a.json().required() })
@@ -2108,6 +2115,7 @@ const baseDefinitions = {
2108
2115
  };
2109
2116
 
2110
2117
  export const schema = a.schema(baseDefinitions).authorization((allow) => [
2118
+ allow.resource(assignmentAction).to(["query", "mutate"]),
2111
2119
  allow.resource(categoryAction),
2112
2120
  allow.resource(contentActions),
2113
2121
  allow.resource(manageUserRole),
@@ -2119,6 +2127,7 @@ export const schema = a.schema(baseDefinitions).authorization((allow) => [
2119
2127
  /** The deployed schema of a site that enables plugins: the base definitions plus what each plugin contributes (docs/plugins.md). */
2120
2128
  export function schemaWithPlugins(pluginDefinitions: Record<string, any>) {
2121
2129
  return a.schema({ ...baseDefinitions, ...pluginDefinitions } as typeof baseDefinitions).authorization((allow) => [
2130
+ allow.resource(assignmentAction).to(["query", "mutate"]),
2122
2131
  allow.resource(categoryAction),
2123
2132
  allow.resource(contentActions),
2124
2133
  allow.resource(manageUserRole),
@@ -0,0 +1,88 @@
1
+ import { Aspects, Stack } from "aws-cdk-lib";
2
+ import { CfnPolicy } from "aws-cdk-lib/aws-iam";
3
+ /** Lifecycle/context calls, including the fixed reference registration CLI's provenance operations. */
4
+ export const ASSIGNMENT_DATA_FIELDS = [
5
+ "Query/fields/getAssignment",
6
+ "Query/fields/getKnowledgeRawPayload",
7
+ "Query/fields/getProcedureRun",
8
+ "Query/fields/getProcedureVersion",
9
+ "Query/fields/getProcedureDefinition",
10
+ "Query/fields/getModelAttachment",
11
+ "Query/fields/getNewsroomSection",
12
+ "Query/fields/getKnowledgeCorpus",
13
+ "Query/fields/getKnowledgeImportRun",
14
+ "Query/fields/getReference",
15
+ "Query/fields/getReferenceAttachment",
16
+ "Query/fields/getMessage",
17
+ "Query/fields/getSemanticRelation",
18
+ "Query/fields/getSemanticNode",
19
+ "Query/fields/listReferences",
20
+ "Query/fields/listReferenceAttachments",
21
+ "Query/fields/listSemanticRelations",
22
+ "Query/fields/listKnowledgeCorporaByRoleAndName",
23
+ "Query/fields/listItemsByLineageAndVersion",
24
+ "Query/fields/itemBySlug",
25
+ "Query/fields/itemByTenantSlug",
26
+ "Query/fields/listModelAttachmentsByOwnerRoleAndSortKey",
27
+ "Query/fields/listAssignmentsByQueueStatusAndPriority",
28
+ "Query/fields/listSemanticRelationsByObjectState",
29
+ "Query/fields/listSemanticRelationsBySubjectState",
30
+ "Query/fields/listAssignmentEventsByAssignmentAndCreatedAt",
31
+ "Mutation/fields/createAssignment",
32
+ "Mutation/fields/updateAssignment",
33
+ "Mutation/fields/createAssignmentEvent",
34
+ "Mutation/fields/createProcedureRun",
35
+ "Mutation/fields/updateProcedureRun",
36
+ "Mutation/fields/createKnowledgeCorpus",
37
+ "Mutation/fields/updateKnowledgeCorpus",
38
+ "Mutation/fields/createKnowledgeImportRun",
39
+ "Mutation/fields/updateKnowledgeImportRun",
40
+ "Mutation/fields/createKnowledgeRawPayload",
41
+ "Mutation/fields/updateKnowledgeRawPayload",
42
+ "Mutation/fields/createReference",
43
+ "Mutation/fields/updateReference",
44
+ "Mutation/fields/createReferenceAttachment",
45
+ "Mutation/fields/updateReferenceAttachment",
46
+ "Mutation/fields/createMessage",
47
+ "Mutation/fields/updateMessage",
48
+ "Mutation/fields/createSemanticRelation",
49
+ "Mutation/fields/updateSemanticRelation",
50
+ "Mutation/fields/createSemanticNode",
51
+ "Mutation/fields/updateSemanticNode",
52
+ "Mutation/fields/createModelAttachment",
53
+ "Mutation/fields/updateModelAttachment",
54
+ "Mutation/fields/createModelAttachmentUpload",
55
+ "Mutation/fields/completeModelAttachmentUpload",
56
+ "Mutation/fields/createModelAttachmentDownload",
57
+ "Mutation/fields/updateNewsroomSummary",
58
+ ];
59
+ /** Keep the SDK's generated configuration, replacing only this role's GraphQL grants at synthesis. */
60
+ export function scopeAssignmentDataAccess(lambda, graphqlApiArn) {
61
+ if (!lambda.role)
62
+ throw new Error("Assignment function needs an execution role.");
63
+ const roleName = lambda.role.roleName;
64
+ const resources = ASSIGNMENT_DATA_FIELDS.map((field) => `${graphqlApiArn}/types/${field}`);
65
+ Aspects.of(Stack.of(lambda)).add({
66
+ visit(node) {
67
+ if (!(node instanceof CfnPolicy))
68
+ return;
69
+ const stack = Stack.of(node);
70
+ const roles = stack.resolve(node.roles);
71
+ if (!roles?.some((role) => JSON.stringify(role) === JSON.stringify(stack.resolve(roleName))))
72
+ return;
73
+ const document = stack.resolve(node.policyDocument);
74
+ document.Statement.forEach((statement, index) => {
75
+ const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
76
+ if (actions.includes("*") || actions.includes("appsync:*")) {
77
+ throw new Error("Unexpected wildcard assignment action; review permissions before synthesis.");
78
+ }
79
+ if (!actions.includes("appsync:GraphQL"))
80
+ return;
81
+ if (statement.Effect !== "Allow" || actions.length !== 1 || statement.NotResource) {
82
+ throw new Error("Unexpected assignment GraphQL policy; review field scoping before synthesis.");
83
+ }
84
+ node.addPropertyOverride(`PolicyDocument.Statement.${index}.Resource`, resources);
85
+ });
86
+ },
87
+ });
88
+ }
@@ -0,0 +1,87 @@
1
+ import { Aspects, Stack } from "aws-cdk-lib";
2
+ import { CfnPolicy } from "aws-cdk-lib/aws-iam";
3
+ import type { IFunction } from "aws-cdk-lib/aws-lambda";
4
+
5
+ /** Lifecycle/context calls, including the fixed reference registration CLI's provenance operations. */
6
+ export const ASSIGNMENT_DATA_FIELDS = [
7
+ "Query/fields/getAssignment",
8
+ "Query/fields/getKnowledgeRawPayload",
9
+ "Query/fields/getProcedureRun",
10
+ "Query/fields/getProcedureVersion",
11
+ "Query/fields/getProcedureDefinition",
12
+ "Query/fields/getModelAttachment",
13
+ "Query/fields/getNewsroomSection",
14
+ "Query/fields/getKnowledgeCorpus",
15
+ "Query/fields/getKnowledgeImportRun",
16
+ "Query/fields/getReference",
17
+ "Query/fields/getReferenceAttachment",
18
+ "Query/fields/getMessage",
19
+ "Query/fields/getSemanticRelation",
20
+ "Query/fields/getSemanticNode",
21
+ "Query/fields/listReferences",
22
+ "Query/fields/listReferenceAttachments",
23
+ "Query/fields/listSemanticRelations",
24
+ "Query/fields/listKnowledgeCorporaByRoleAndName",
25
+ "Query/fields/listItemsByLineageAndVersion",
26
+ "Query/fields/itemBySlug",
27
+ "Query/fields/itemByTenantSlug",
28
+ "Query/fields/listModelAttachmentsByOwnerRoleAndSortKey",
29
+ "Query/fields/listAssignmentsByQueueStatusAndPriority",
30
+ "Query/fields/listSemanticRelationsByObjectState",
31
+ "Query/fields/listSemanticRelationsBySubjectState",
32
+ "Query/fields/listAssignmentEventsByAssignmentAndCreatedAt",
33
+ "Mutation/fields/createAssignment",
34
+ "Mutation/fields/updateAssignment",
35
+ "Mutation/fields/createAssignmentEvent",
36
+ "Mutation/fields/createProcedureRun",
37
+ "Mutation/fields/updateProcedureRun",
38
+ "Mutation/fields/createKnowledgeCorpus",
39
+ "Mutation/fields/updateKnowledgeCorpus",
40
+ "Mutation/fields/createKnowledgeImportRun",
41
+ "Mutation/fields/updateKnowledgeImportRun",
42
+ "Mutation/fields/createKnowledgeRawPayload",
43
+ "Mutation/fields/updateKnowledgeRawPayload",
44
+ "Mutation/fields/createReference",
45
+ "Mutation/fields/updateReference",
46
+ "Mutation/fields/createReferenceAttachment",
47
+ "Mutation/fields/updateReferenceAttachment",
48
+ "Mutation/fields/createMessage",
49
+ "Mutation/fields/updateMessage",
50
+ "Mutation/fields/createSemanticRelation",
51
+ "Mutation/fields/updateSemanticRelation",
52
+ "Mutation/fields/createSemanticNode",
53
+ "Mutation/fields/updateSemanticNode",
54
+ "Mutation/fields/createModelAttachment",
55
+ "Mutation/fields/updateModelAttachment",
56
+ "Mutation/fields/createModelAttachmentUpload",
57
+ "Mutation/fields/completeModelAttachmentUpload",
58
+ "Mutation/fields/createModelAttachmentDownload",
59
+ "Mutation/fields/updateNewsroomSummary",
60
+ ] as const;
61
+
62
+ /** Keep the SDK's generated configuration, replacing only this role's GraphQL grants at synthesis. */
63
+ export function scopeAssignmentDataAccess(lambda: IFunction, graphqlApiArn: string): void {
64
+ if (!lambda.role) throw new Error("Assignment function needs an execution role.");
65
+ const roleName = lambda.role.roleName;
66
+ const resources = ASSIGNMENT_DATA_FIELDS.map((field) => `${graphqlApiArn}/types/${field}`);
67
+ Aspects.of(Stack.of(lambda)).add({
68
+ visit(node) {
69
+ if (!(node instanceof CfnPolicy)) return;
70
+ const stack = Stack.of(node);
71
+ const roles = stack.resolve(node.roles) as unknown[] | undefined;
72
+ if (!roles?.some((role) => JSON.stringify(role) === JSON.stringify(stack.resolve(roleName)))) return;
73
+ const document = stack.resolve(node.policyDocument) as { Statement: Array<Record<string, unknown>> };
74
+ document.Statement.forEach((statement, index) => {
75
+ const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
76
+ if (actions.includes("*") || actions.includes("appsync:*")) {
77
+ throw new Error("Unexpected wildcard assignment action; review permissions before synthesis.");
78
+ }
79
+ if (!actions.includes("appsync:GraphQL")) return;
80
+ if (statement.Effect !== "Allow" || actions.length !== 1 || statement.NotResource) {
81
+ throw new Error("Unexpected assignment GraphQL policy; review field scoping before synthesis.");
82
+ }
83
+ node.addPropertyOverride(`PolicyDocument.Statement.${index}.Resource`, resources);
84
+ });
85
+ },
86
+ });
87
+ }
@@ -23,6 +23,7 @@ from papyrus_content.rebuild_trigger import (
23
23
  trigger_staging_build,
24
24
  )
25
25
  from papyrus_content.story_reporting import save_story_version
26
+ from papyrus_content.reference_intake import IntakeError, intake_references
26
27
  from papyrus_content.publishing import (
27
28
  ItemFields,
28
29
  PublishError,
@@ -299,6 +300,12 @@ def handler(event, context):
299
300
  arguments = parse_input(event["arguments"]["input"])
300
301
  actor = actor_from_event(event)
301
302
  try:
303
+ if field == "intakePendingReferences":
304
+ client, _claims = create_authoring_client()
305
+ try:
306
+ return intake_references(client, arguments, event.get("identity") or {})
307
+ except IntakeError as error:
308
+ return {"ok": False, "errors": [{"code": "intake-rejected", "message": str(error)}]}
302
309
  if field == "deriveMarkus":
303
310
  return derive_markus_action(arguments)
304
311
  if field == "saveItemDraft":
@@ -49,6 +49,7 @@ const ALLOWED_ROLES = new Set([
49
49
  "graph_export",
50
50
  "raw_payload",
51
51
  "priming_outcome",
52
+ "priming_attempts",
52
53
  ]);
53
54
  const ALLOWED_MEDIA_TYPES = new Set([
54
55
  "application/json",
@@ -0,0 +1,14 @@
1
+ """The editorial-checks plugin's function: routes its GraphQL fields to papyrus_content.editorial_checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from papyrus_content.editorial_checks import GraphQLEditorialStore, Services, handle, load_provider_keys
8
+
9
+ # Amplify passes secrets to Python functions as placeholders; resolve them once per cold start.
10
+ load_provider_keys()
11
+
12
+
13
+ def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
14
+ return handle(event["info"]["fieldName"], event.get("arguments") or {}, Services(store=GraphQLEditorialStore()))
@@ -0,0 +1 @@
1
+ limatus[jev]>=0.31,<0.32
@@ -0,0 +1,43 @@
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 { execFileSync } from "node:child_process";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { pythonLambdaCode } from "../../../functions/shared/python-bundle.js";
8
+ const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
9
+ /** AppSync gives a resolver 30 seconds; Limatus's classifier deadline (settings, default 20 s) keeps a check inside this. */
10
+ export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
11
+ /** Limatus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
12
+ export const editorialChecksRequirements = ["limatus[jev]>=0.31,<0.32"];
13
+ function installRequirements(outputDir) {
14
+ execFileSync("python3", [
15
+ "-m", "pip", "install", "-r", path.join(functionDirectory, "requirements.txt"), "-t", outputDir, "--no-cache-dir",
16
+ "--platform", "manylinux2014_aarch64", "--implementation", "cp", "--python-version", "3.12",
17
+ "--only-binary=:all:", "--ignore-requires-python",
18
+ ], { stdio: "inherit" });
19
+ }
20
+ export const editorialChecks = defineFunction((scope) => new Function(scope, "papyrus-editorial-checks", {
21
+ runtime: Runtime.PYTHON_3_12,
22
+ architecture: Architecture.ARM_64,
23
+ handler: "handler.handler",
24
+ timeout: Duration.seconds(EDITORIAL_CHECKS_TIMEOUT_SECONDS),
25
+ memorySize: 1024,
26
+ code: pythonLambdaCode({
27
+ functionDir: "../plugins/editorial-checks/function",
28
+ modules: ["papyrus_content"],
29
+ requirements: editorialChecksRequirements,
30
+ noDeps: true,
31
+ checkoutExtras: installRequirements,
32
+ }, [
33
+ "bash",
34
+ "-c",
35
+ [
36
+ "set -euo pipefail",
37
+ "mkdir -p /asset-output/papyrus_content",
38
+ "cp amplify/plugins/editorial-checks/function/handler.py /asset-output/handler.py",
39
+ "cp -R src/papyrus_content/. /asset-output/papyrus_content/",
40
+ "python3 -m pip install -r amplify/plugins/editorial-checks/function/requirements.txt -t /asset-output --no-cache-dir",
41
+ ].join(" && "),
42
+ ]),
43
+ }), { resourceGroupName: "data" });
@@ -0,0 +1,60 @@
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 { execFileSync } from "node:child_process";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { pythonLambdaCode } from "../../../functions/shared/python-bundle";
9
+
10
+ const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ /** AppSync gives a resolver 30 seconds; Limatus's classifier deadline (settings, default 20 s) keeps a check inside this. */
13
+ export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
14
+
15
+ /** Limatus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
16
+ export const editorialChecksRequirements = ["limatus[jev]>=0.31,<0.32"];
17
+
18
+ function installRequirements(outputDir: string): void {
19
+ execFileSync(
20
+ "python3",
21
+ [
22
+ "-m", "pip", "install", "-r", path.join(functionDirectory, "requirements.txt"), "-t", outputDir, "--no-cache-dir",
23
+ "--platform", "manylinux2014_aarch64", "--implementation", "cp", "--python-version", "3.12",
24
+ "--only-binary=:all:", "--ignore-requires-python",
25
+ ],
26
+ { stdio: "inherit" },
27
+ );
28
+ }
29
+
30
+ export const editorialChecks = defineFunction(
31
+ (scope: Construct) =>
32
+ new Function(scope, "papyrus-editorial-checks", {
33
+ runtime: Runtime.PYTHON_3_12,
34
+ architecture: Architecture.ARM_64,
35
+ handler: "handler.handler",
36
+ timeout: Duration.seconds(EDITORIAL_CHECKS_TIMEOUT_SECONDS),
37
+ memorySize: 1024,
38
+ code: pythonLambdaCode(
39
+ {
40
+ functionDir: "../plugins/editorial-checks/function",
41
+ modules: ["papyrus_content"],
42
+ requirements: editorialChecksRequirements,
43
+ noDeps: true,
44
+ checkoutExtras: installRequirements,
45
+ },
46
+ [
47
+ "bash",
48
+ "-c",
49
+ [
50
+ "set -euo pipefail",
51
+ "mkdir -p /asset-output/papyrus_content",
52
+ "cp amplify/plugins/editorial-checks/function/handler.py /asset-output/handler.py",
53
+ "cp -R src/papyrus_content/. /asset-output/papyrus_content/",
54
+ "python3 -m pip install -r amplify/plugins/editorial-checks/function/requirements.txt -t /asset-output --no-cache-dir",
55
+ ].join(" && "),
56
+ ],
57
+ ),
58
+ }),
59
+ { resourceGroupName: "data" },
60
+ );
@@ -0,0 +1,71 @@
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
+ };
@@ -0,0 +1,87 @@
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
+ };