@anthusai/papyrus 1.4.0-next.1 → 1.4.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 (121) hide show
  1. package/amplify/data/resource.js +134 -3
  2. package/amplify/data/resource.ts +145 -3
  3. package/amplify/data/schema.js +131 -0
  4. package/amplify/data/schema.ts +142 -0
  5. package/amplify/functions/category-action/handler.ts +272 -20
  6. package/amplify/functions/console-chat-responder/src/main.rs +1 -1
  7. package/amplify/functions/content-actions/handler.py +23 -0
  8. package/amplify/functions/content-actions/requirements.txt +1 -1
  9. package/amplify/functions/content-actions/resource.js +1 -1
  10. package/amplify/functions/content-actions/resource.ts +1 -1
  11. package/amplify/functions/renditions/requirements.txt +1 -1
  12. package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
  13. package/amplify/functions/shared/curation-delegation.ts +115 -0
  14. package/amplify/functions/shared/python-bundle.js +3 -3
  15. package/amplify/functions/shared/python-bundle.ts +3 -3
  16. package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
  17. package/amplify/functions/shared/slack-events.ts +2 -2
  18. package/amplify/publication/renditions-function.js +1 -1
  19. package/amplify/publication/renditions-function.ts +1 -1
  20. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
  21. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
  22. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
  23. package/app/[year]/[month]/[day]/page.tsx +3 -0
  24. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
  25. package/app/api/archive/editions/route.ts +2 -0
  26. package/app/api/auth/prospectus/accounts/route.ts +9 -0
  27. package/app/api/local-preview/derive/route.ts +28 -0
  28. package/app/api/local-preview/media/route.ts +25 -0
  29. package/app/archive/page.tsx +3 -0
  30. package/app/globals.css +28 -28
  31. package/app/layout.tsx +8 -5
  32. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  33. package/app/newsroom/[section]/page.tsx +3 -2
  34. package/app/newsroom/articles/page.tsx +8 -1
  35. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  36. package/app/newsroom/insights/page.tsx +4 -12
  37. package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
  38. package/app/page.tsx +30 -1
  39. package/components/administration-delegations-panel.tsx +156 -0
  40. package/components/curation-delegation-client.ts +104 -0
  41. package/components/item-stream-view.tsx +63 -0
  42. package/components/markus-ir-preview.tsx +47 -2
  43. package/components/news-desk-page.tsx +0 -1
  44. package/components/news-desk-taxonomy-client.ts +82 -242
  45. package/components/newsroom-article-editor.tsx +138 -40
  46. package/components/newsroom-assignment-detail.tsx +79 -0
  47. package/components/newsroom-assignments-view.tsx +1 -1
  48. package/components/newsroom-board-view.tsx +128 -51
  49. package/components/newsroom-newsprint-board.css +5 -46
  50. package/components/newsroom-ops-overview.tsx +27 -5
  51. package/components/newsroom-organizer-shell.tsx +26 -8
  52. package/components/newsroom-organizer.css +267 -12
  53. package/components/newsroom-references-view.tsx +100 -23
  54. package/components/newsroom-topics-view.tsx +1 -1
  55. package/components/priming-proof-page.tsx +17 -8
  56. package/components/prospectus-account-chooser.tsx +131 -0
  57. package/components/reference-cyclotron-status.tsx +5 -5
  58. package/components/reference-cyclotron-suggestion.tsx +143 -0
  59. package/components/reference-live-agreement.tsx +62 -0
  60. package/components/reference-section-suggestion.tsx +104 -0
  61. package/components/story-reporting-trail-client.ts +91 -0
  62. package/components/story-reporting-trail.tsx +115 -0
  63. package/components/topic-steering-workspace.tsx +245 -678
  64. package/components/ui/sheet.tsx +10 -4
  65. package/components/use-paged-content-item-versions.ts +151 -0
  66. package/components/wiki-concepts-view.tsx +153 -0
  67. package/components/wiki-index-client.ts +51 -0
  68. package/infra/amplify-app-shell.js +17 -3
  69. package/infra/amplify-app-shell.ts +18 -3
  70. package/infra/site-config.js +6 -1
  71. package/infra/site-config.ts +7 -1
  72. package/lib/approved-priming-proof-reports.ts +6 -3
  73. package/lib/approved-priming-proofs/papyrus.json +90 -0
  74. package/lib/blog-feature-solver.ts +1 -109
  75. package/lib/category-repository.ts +1 -1
  76. package/lib/content-actions-client.ts +3 -0
  77. package/lib/content-actions-demo.ts +55 -0
  78. package/lib/content-item-lifecycle.ts +34 -5
  79. package/lib/content-source-context.ts +9 -1
  80. package/lib/curation-delegation-helpers.ts +69 -0
  81. package/lib/cyclotron-suggestions.ts +263 -0
  82. package/lib/front-matter-fields.ts +42 -0
  83. package/lib/front-mosaic.ts +2 -1
  84. package/lib/graphql-content-repository.ts +42 -7
  85. package/lib/item-stream.ts +47 -0
  86. package/lib/layout-plan.ts +3 -2
  87. package/lib/layout-warnings.ts +8 -0
  88. package/lib/local-preview-section-fixtures.ts +72 -0
  89. package/lib/local-preview-seed.ts +196 -7
  90. package/lib/local-preview-tenant.ts +27 -0
  91. package/lib/newsroom-articles.ts +2 -0
  92. package/lib/newsroom-assignments.ts +14 -1
  93. package/lib/newsroom-board.ts +52 -7
  94. package/lib/newsroom-demo-dashboard.ts +1 -1
  95. package/lib/newsroom-forum-routes.ts +2 -43
  96. package/lib/newsroom-index-filters.ts +3 -40
  97. package/lib/newsroom-media-backend.ts +5 -0
  98. package/lib/newsroom-nav.ts +1 -3
  99. package/lib/newsroom-organizer-nav.ts +8 -14
  100. package/lib/newsroom-section-routing.ts +1 -1
  101. package/lib/newsroom-sections.ts +12 -0
  102. package/lib/papyrus-web-locations.ts +3 -24
  103. package/lib/priming-proof-report.ts +1 -0
  104. package/lib/prospectus-account-client.ts +90 -0
  105. package/lib/prospectus-sign-in-routes.ts +45 -0
  106. package/lib/prospectus-site-identity.ts +13 -1
  107. package/lib/prospectus-tenant.ts +22 -0
  108. package/lib/reader-static-params.ts +2 -1
  109. package/lib/relevance-decisions.ts +29 -0
  110. package/lib/reporting-story-budget.ts +2 -2
  111. package/lib/semantic-graph.ts +5 -5
  112. package/lib/site-brand.ts +20 -0
  113. package/lib/site-env.ts +5 -0
  114. package/lib/site-section-suggestions.ts +120 -0
  115. package/lib/site-stack.ts +1 -1
  116. package/lib/story-reporting-trail.ts +106 -0
  117. package/lib/tenant-keys.ts +32 -0
  118. package/lib/wiki.ts +56 -0
  119. package/package.json +34 -2
  120. package/renderers/pretext/rhythm-article-page.tsx +1 -18
  121. package/routes.manifest.json +34 -7
@@ -4,12 +4,39 @@ import { Amplify } from "aws-amplify";
4
4
  import { generateClient } from "aws-amplify/data";
5
5
  import type { Schema } from "../../data/resource";
6
6
  import { putJsonModelPayload, putTextModelPayload, readJsonModelPayload } from "../shared/model-payloads";
7
- import { referenceCurationMetadata } from "../shared/reference-curation-metadata";
7
+ import { curationActorKind, referenceCurationMetadata, validatedCurationSectionId } from "../shared/reference-curation-metadata";
8
+ import {
9
+ CURATION_DELEGATION_OWNER_TYPE,
10
+ CURATION_DELEGATION_PAYLOAD_KIND,
11
+ delegationExpiry,
12
+ delegationRefusalMessage,
13
+ delegationVerdict,
14
+ newDelegationToken,
15
+ PENDING_REQUEST_GRANTOR_SUB,
16
+ type CurationDelegation,
17
+ } from "../shared/curation-delegation";
18
+ import {
19
+ approveDelegation,
20
+ delegationResult as flowDelegationResult,
21
+ delegationStatus,
22
+ denyDelegation,
23
+ livePendingRequests,
24
+ requestDelegation,
25
+ type DelegationStore,
26
+ } from "../shared/curation-delegation-flow";
8
27
 
9
28
  type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
10
29
  type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
30
+ type CreateCurationDelegationHandler = Schema["createCurationDelegation"]["functionHandler"];
31
+ type RevokeCurationDelegationHandler = Schema["revokeCurationDelegation"]["functionHandler"];
32
+ type ListCurationDelegationsHandler = Schema["listCurationDelegations"]["functionHandler"];
33
+ type RequestCurationDelegationHandler = Schema["requestCurationDelegation"]["functionHandler"];
34
+ type ApproveCurationDelegationHandler = Schema["approveCurationDelegation"]["functionHandler"];
35
+ type DenyCurationDelegationHandler = Schema["denyCurationDelegation"]["functionHandler"];
36
+ type GetCurationDelegationStatusHandler = Schema["getCurationDelegationStatus"]["functionHandler"];
37
+ type RecordDelegatedCurationHandler = Schema["recordDelegatedCuration"]["functionHandler"];
11
38
  type ReferenceQualityHandler = Schema["setReferenceQualityRating"]["functionHandler"];
12
- type ReferenceInsightHandler = Schema["createReferenceInsight"]["functionHandler"];
39
+ type ReferenceNoteHandler = Schema["createReferenceNote"]["functionHandler"];
13
40
  type MoveReferenceCorpusHandler = Schema["moveReferenceCorpus"]["functionHandler"];
14
41
  type StartReferenceCurationHandler = Schema["startReferenceCuration"]["functionHandler"];
15
42
  type GetReferenceCurationStatusHandler = Schema["getReferenceCurationStatus"]["functionHandler"];
@@ -23,8 +50,16 @@ type ReviewReferenceTopicLabelHandler = Schema["reviewReferenceTopicLabel"]["fun
23
50
  type CategoryActionEvent =
24
51
  | Parameters<ReviewHandler>[0]
25
52
  | Parameters<ReferenceCurationHandler>[0]
53
+ | Parameters<CreateCurationDelegationHandler>[0]
54
+ | Parameters<RevokeCurationDelegationHandler>[0]
55
+ | Parameters<ListCurationDelegationsHandler>[0]
56
+ | Parameters<RequestCurationDelegationHandler>[0]
57
+ | Parameters<ApproveCurationDelegationHandler>[0]
58
+ | Parameters<DenyCurationDelegationHandler>[0]
59
+ | Parameters<GetCurationDelegationStatusHandler>[0]
60
+ | Parameters<RecordDelegatedCurationHandler>[0]
26
61
  | Parameters<ReferenceQualityHandler>[0]
27
- | Parameters<ReferenceInsightHandler>[0]
62
+ | Parameters<ReferenceNoteHandler>[0]
28
63
  | Parameters<MoveReferenceCorpusHandler>[0]
29
64
  | Parameters<StartReferenceCurationHandler>[0]
30
65
  | Parameters<GetReferenceCurationStatusHandler>[0]
@@ -88,8 +123,16 @@ export const handler = async (event: CategoryActionEvent) => {
88
123
  }
89
124
  if (fieldName === "reviewSteeringProposal") return reviewSteeringProposal(event as Parameters<ReviewHandler>[0]);
90
125
  if (fieldName === "reviewReferenceCuration") return reviewReferenceCuration(event as Parameters<ReferenceCurationHandler>[0]);
126
+ if (fieldName === "createCurationDelegation") return createCurationDelegation(event as Parameters<CreateCurationDelegationHandler>[0]);
127
+ if (fieldName === "revokeCurationDelegation") return revokeCurationDelegation(event as Parameters<RevokeCurationDelegationHandler>[0]);
128
+ if (fieldName === "listCurationDelegations") return listCurationDelegations(event as Parameters<ListCurationDelegationsHandler>[0]);
129
+ if (fieldName === "requestCurationDelegation") return requestCurationDelegationAction(event as Parameters<RequestCurationDelegationHandler>[0]);
130
+ if (fieldName === "approveCurationDelegation") return approveCurationDelegationAction(event as Parameters<ApproveCurationDelegationHandler>[0]);
131
+ if (fieldName === "denyCurationDelegation") return denyCurationDelegationAction(event as Parameters<DenyCurationDelegationHandler>[0]);
132
+ if (fieldName === "getCurationDelegationStatus") return getCurationDelegationStatusAction(event as Parameters<GetCurationDelegationStatusHandler>[0]);
133
+ if (fieldName === "recordDelegatedCuration") return recordDelegatedCuration(event as Parameters<RecordDelegatedCurationHandler>[0]);
91
134
  if (fieldName === "setReferenceQualityRating") return setReferenceQualityRating(event as Parameters<ReferenceQualityHandler>[0]);
92
- if (fieldName === "createReferenceInsight") return createReferenceInsight(event as Parameters<ReferenceInsightHandler>[0]);
135
+ if (fieldName === "createReferenceNote") return createReferenceNote(event as Parameters<ReferenceNoteHandler>[0]);
93
136
  if (fieldName === "moveReferenceCorpus") return moveReferenceCorpus(event as Parameters<MoveReferenceCorpusHandler>[0]);
94
137
  if (fieldName === "startReferenceCuration") return startReferenceCuration(event as Parameters<StartReferenceCurationHandler>[0]);
95
138
  if (fieldName === "getReferenceCurationStatus") return getReferenceCurationStatus(event as Parameters<GetReferenceCurationStatusHandler>[0]);
@@ -162,13 +205,21 @@ async function reviewSteeringProposal(event: Parameters<ReviewHandler>[0]) {
162
205
  };
163
206
  }
164
207
 
165
- async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandler>[0]) {
208
+ async function reviewReferenceCuration(
209
+ event: Parameters<ReferenceCurationHandler>[0],
210
+ delegation: { delegationId: string; relayedBy: string; instruction: string } | null = null,
211
+ ) {
166
212
  const client = await getDataClient();
167
213
  const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
168
214
  const action = normalizeReferenceCurationAction(event.arguments.action);
169
215
  const nextStatus = referenceCurationStatusForAction(action);
170
216
  const reasonCode = normalizeReferenceRejectionReasonCode(event.arguments.reasonCode, action === "reject");
171
217
  const reference = await getRequiredRecord(client.models.Reference, referenceId, "Reference");
218
+ const requestedSectionId = normalizeOptionalString(event.arguments.sectionId);
219
+ const sectionRecord = requestedSectionId
220
+ ? (await client.models.NewsroomSection.get({ id: requestedSectionId })).data ?? null
221
+ : null;
222
+ const sectionId = validatedCurationSectionId(requestedSectionId, action, sectionRecord);
172
223
  const now = new Date().toISOString();
173
224
  const actorSub = normalizeOptionalString(event.arguments.actorSub) ?? getIdentitySub(event);
174
225
  const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
@@ -234,6 +285,9 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
234
285
  referenceLineageId,
235
286
  decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
236
287
  shareable: event.arguments.shareable ?? null,
288
+ actorKind: delegation ? "human" : curationActorKind(event.identity),
289
+ sectionId,
290
+ delegation,
237
291
  }),
238
292
  { filename: "metadata.json", now },
239
293
  );
@@ -300,6 +354,204 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
300
354
  };
301
355
  }
302
356
 
357
+ function requireSignedInPerson(event: { identity?: unknown }): { sub: string; label: string; isAdmin: boolean } {
358
+ if (curationActorKind(event.identity) !== "human") {
359
+ throw new Error("Only a person signed in to the Newsroom can manage curation delegations.");
360
+ }
361
+ const identity = event.identity as { sub?: unknown; claims?: Record<string, unknown> };
362
+ const claims = identity.claims ?? {};
363
+ const groups = Array.isArray(claims["cognito:groups"]) ? (claims["cognito:groups"] as unknown[]) : [];
364
+ return {
365
+ sub: normalizeRequiredString(claims.sub ?? identity.sub, "identity.sub"),
366
+ label: normalizeOptionalString(claims.email) ?? normalizeOptionalString(claims.name) ?? "Newsroom editor",
367
+ isAdmin: groups.includes("admin"),
368
+ };
369
+ }
370
+
371
+ async function readCurationDelegation(client: DataClient, delegationId: string): Promise<CurationDelegation | null> {
372
+ const stored = await readJsonModelPayload(client as any, "knowledgeRawPayload", delegationId, "raw_payload", CURATION_DELEGATION_PAYLOAD_KIND);
373
+ return stored ? (stored as unknown as CurationDelegation) : null;
374
+ }
375
+
376
+ async function writeCurationDelegation(client: DataClient, delegation: CurationDelegation, now: string, created: boolean) {
377
+ const input = {
378
+ id: delegation.delegationId,
379
+ ownerType: CURATION_DELEGATION_OWNER_TYPE,
380
+ ownerId: delegation.grantorSub,
381
+ payloadKind: CURATION_DELEGATION_PAYLOAD_KIND,
382
+ createdAt: delegation.createdAt,
383
+ updatedAt: now,
384
+ };
385
+ if (created) {
386
+ await requireDataResult(client.models.KnowledgeRawPayload.create(input), "create curation delegation");
387
+ } else {
388
+ await requireDataResult(client.models.KnowledgeRawPayload.update(input), "update curation delegation");
389
+ }
390
+ await putJsonModelPayload(
391
+ client as any,
392
+ { ownerKind: "knowledgeRawPayload", ownerId: delegation.delegationId },
393
+ "raw_payload",
394
+ CURATION_DELEGATION_PAYLOAD_KIND,
395
+ delegation as unknown as Record<string, unknown>,
396
+ { filename: "curation-delegation.json", now },
397
+ );
398
+ }
399
+
400
+ function delegationResult(delegation: CurationDelegation, token: string | null) {
401
+ return { ...flowDelegationResult(delegation, new Date().toISOString()), token };
402
+ }
403
+
404
+ async function listPendingCurationDelegations(client: DataClient): Promise<CurationDelegation[]> {
405
+ const pending: CurationDelegation[] = [];
406
+ let nextToken: string | null | undefined;
407
+ do {
408
+ const page: any = await (client.models.KnowledgeRawPayload as any).listKnowledgeRawPayloadsByOwnerAndKind(
409
+ { ownerId: PENDING_REQUEST_GRANTOR_SUB, payloadKind: { eq: CURATION_DELEGATION_PAYLOAD_KIND } },
410
+ { nextToken },
411
+ );
412
+ assertNoDataErrors(page.errors, "list pending curation delegations");
413
+ for (const payload of page.data ?? []) {
414
+ const delegation = await readCurationDelegation(client, payload.id);
415
+ if (delegation) pending.push(delegation);
416
+ }
417
+ nextToken = page.nextToken;
418
+ } while (nextToken);
419
+ return pending;
420
+ }
421
+
422
+ function curationDelegationStore(client: DataClient): DelegationStore {
423
+ return {
424
+ read: (delegationId) => readCurationDelegation(client, delegationId),
425
+ write: (delegation, now, created) => writeCurationDelegation(client, delegation, now, created),
426
+ listPending: () => listPendingCurationDelegations(client),
427
+ };
428
+ }
429
+
430
+ async function requestCurationDelegationAction(event: Parameters<RequestCurationDelegationHandler>[0]) {
431
+ const client = await getDataClient();
432
+ return requestDelegation(
433
+ curationDelegationStore(client),
434
+ {
435
+ delegationId: normalizeRequiredString(event.arguments.delegationId, "delegationId"),
436
+ tokenHash: normalizeRequiredString(event.arguments.tokenHash, "tokenHash"),
437
+ agentLabel: normalizeRequiredString(event.arguments.agentLabel, "agentLabel"),
438
+ hours: event.arguments.hours,
439
+ },
440
+ new Date().toISOString(),
441
+ );
442
+ }
443
+
444
+ async function approveCurationDelegationAction(event: Parameters<ApproveCurationDelegationHandler>[0]) {
445
+ const person = requireSignedInPerson(event);
446
+ const client = await getDataClient();
447
+ const now = new Date().toISOString();
448
+ return { ...(await approveDelegation(curationDelegationStore(client), person, normalizeRequiredString(event.arguments.userCode, "userCode"), now)), token: null };
449
+ }
450
+
451
+ async function denyCurationDelegationAction(event: Parameters<DenyCurationDelegationHandler>[0]) {
452
+ const person = requireSignedInPerson(event);
453
+ const client = await getDataClient();
454
+ const now = new Date().toISOString();
455
+ return { ...(await denyDelegation(curationDelegationStore(client), person, normalizeRequiredString(event.arguments.userCode, "userCode"), now)), token: null };
456
+ }
457
+
458
+ async function getCurationDelegationStatusAction(event: Parameters<GetCurationDelegationStatusHandler>[0]) {
459
+ const client = await getDataClient();
460
+ return delegationStatus(curationDelegationStore(client), normalizeRequiredString(event.arguments.delegationId, "delegationId"), new Date().toISOString());
461
+ }
462
+
463
+ async function createCurationDelegation(event: Parameters<CreateCurationDelegationHandler>[0]) {
464
+ const person = requireSignedInPerson(event);
465
+ const client = await getDataClient();
466
+ const now = new Date().toISOString();
467
+ const delegationId = `curation-delegation-${randomUUID()}`;
468
+ const { token, tokenHash } = newDelegationToken(delegationId);
469
+ const delegation: CurationDelegation = {
470
+ delegationId,
471
+ tokenHash,
472
+ grantorSub: person.sub,
473
+ grantorLabel: person.label,
474
+ agentLabel: normalizeRequiredString(event.arguments.agentLabel, "agentLabel"),
475
+ createdAt: now,
476
+ expiresAt: delegationExpiry(now, event.arguments.hours),
477
+ revokedAt: null,
478
+ };
479
+ await writeCurationDelegation(client, delegation, now, true);
480
+ return delegationResult(delegation, token);
481
+ }
482
+
483
+ async function revokeCurationDelegation(event: Parameters<RevokeCurationDelegationHandler>[0]) {
484
+ const person = requireSignedInPerson(event);
485
+ const client = await getDataClient();
486
+ const delegation = await readCurationDelegation(client, normalizeRequiredString(event.arguments.delegationId, "delegationId"));
487
+ if (!delegation) throw new Error("That delegation was not found.");
488
+ if (delegation.grantorSub !== person.sub && delegation.grantorSub !== PENDING_REQUEST_GRANTOR_SUB && !person.isAdmin) throw new Error("Only the person who granted a delegation can revoke it.");
489
+ const now = new Date().toISOString();
490
+ const revoked = { ...delegation, revokedAt: delegation.revokedAt ?? now };
491
+ await writeCurationDelegation(client, revoked, now, false);
492
+ return delegationResult(revoked, null);
493
+ }
494
+
495
+ async function listCurationDelegations(event: Parameters<ListCurationDelegationsHandler>[0]) {
496
+ const person = requireSignedInPerson(event);
497
+ const client = await getDataClient();
498
+ const delegations: CurationDelegation[] = [];
499
+ let nextToken: string | null | undefined;
500
+ do {
501
+ const page: any = await (client.models.KnowledgeRawPayload as any).listKnowledgeRawPayloadsByOwnerAndKind(
502
+ { ownerId: person.sub, payloadKind: { eq: CURATION_DELEGATION_PAYLOAD_KIND } },
503
+ { nextToken },
504
+ );
505
+ assertNoDataErrors(page.errors, "list curation delegations");
506
+ for (const payload of page.data ?? []) {
507
+ const delegation = await readCurationDelegation(client, payload.id);
508
+ if (delegation) delegations.push(delegation);
509
+ }
510
+ nextToken = page.nextToken;
511
+ } while (nextToken);
512
+ const now = new Date().toISOString();
513
+ delegations.push(...(await livePendingRequests(curationDelegationStore(client), now)));
514
+ return delegations
515
+ .sort((first, second) => second.createdAt.localeCompare(first.createdAt))
516
+ .map((delegation) => delegationResult(delegation, null));
517
+ }
518
+
519
+ async function recordDelegatedCuration(event: Parameters<RecordDelegatedCurationHandler>[0]) {
520
+ const client = await getDataClient();
521
+ const now = new Date().toISOString();
522
+ const delegationId = normalizeRequiredString(event.arguments.delegationId, "delegationId");
523
+ const agentLabel = normalizeRequiredString(event.arguments.agentLabel, "agentLabel");
524
+ const delegation = await readCurationDelegation(client, delegationId);
525
+ const verdict = delegationVerdict(delegation, {
526
+ token: normalizeRequiredString(event.arguments.delegationToken, "delegationToken"),
527
+ agentLabel,
528
+ now,
529
+ });
530
+ if (!verdict.ok || !delegation) {
531
+ throw new Error(delegationRefusalMessage(verdict.ok ? "unknown-token" : verdict.reason));
532
+ }
533
+ const instruction = normalizeRequiredString(event.arguments.instruction, "instruction").slice(0, 2000);
534
+ const namedPerson = normalizeRequiredString(event.arguments.onBehalfOf, "onBehalfOf");
535
+ if (namedPerson.toLowerCase() !== delegation.grantorLabel.toLowerCase()) {
536
+ throw new Error(`That delegation was granted by ${delegation.grantorLabel}, not ${namedPerson}.`);
537
+ }
538
+ const onBehalfOfPerson = {
539
+ ...event,
540
+ arguments: {
541
+ referenceId: event.arguments.referenceId,
542
+ action: event.arguments.action,
543
+ note: event.arguments.note,
544
+ reasonCode: event.arguments.reasonCode,
545
+ decisionRelationId: event.arguments.decisionRelationId,
546
+ shareable: event.arguments.shareable,
547
+ sectionId: event.arguments.sectionId,
548
+ actorSub: delegation.grantorSub,
549
+ actorLabel: delegation.grantorLabel,
550
+ },
551
+ } as unknown as Parameters<ReferenceCurationHandler>[0];
552
+ return reviewReferenceCuration(onBehalfOfPerson, { delegationId, relayedBy: agentLabel, instruction });
553
+ }
554
+
303
555
  async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandler>[0]) {
304
556
  const client = await getDataClient();
305
557
  const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
@@ -403,7 +655,7 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
403
655
  };
404
656
  }
405
657
 
406
- async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>[0]) {
658
+ async function createReferenceNote(event: Parameters<ReferenceNoteHandler>[0]) {
407
659
  const client = await getDataClient();
408
660
  const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
409
661
  const summary = normalizeRequiredString(event.arguments.summary, "summary");
@@ -414,10 +666,10 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
414
666
  const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
415
667
  const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
416
668
  const referenceLineageId = normalizeOptionalString(reference.lineageId) ?? referenceId;
417
- const messageId = `message-reference-insight-${safeId(referenceLineageId)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
669
+ const messageId = `message-reference-note-${safeId(referenceLineageId)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
418
670
  const relationId = `semantic-relation-${hashStable([
419
671
  `message#${messageId}`,
420
- "insight_about",
672
+ "note_about",
421
673
  `reference#${referenceId}`,
422
674
  ]).slice(0, 24)}`;
423
675
 
@@ -425,7 +677,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
425
677
  client.models.Message.create({
426
678
  ...buildCanonicalMessageInput({
427
679
  id: messageId,
428
- messageKind: "insight",
680
+ messageKind: "note",
429
681
  messageDomain: "knowledge",
430
682
  summary,
431
683
  source: "newsroom",
@@ -436,7 +688,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
436
688
  threadId: messageId,
437
689
  sequenceNumber: 1,
438
690
  }),
439
- "create insight Message",
691
+ "create note Message",
440
692
  );
441
693
  await putTextModelPayload(
442
694
  client as any,
@@ -452,7 +704,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
452
704
  "metadata",
453
705
  "metadata",
454
706
  {
455
- kind: "reference.insight.created",
707
+ kind: "reference.note.created",
456
708
  targetKind: "reference",
457
709
  targetId: referenceId,
458
710
  targetLineageId: referenceLineageId,
@@ -463,8 +715,8 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
463
715
  client.models.SemanticRelation.create({
464
716
  id: relationId,
465
717
  relationState: "current",
466
- predicate: "insight_about",
467
- ...semanticRelationTypeFieldsForPredicate("insight_about"),
718
+ predicate: "note_about",
719
+ ...semanticRelationTypeFieldsForPredicate("note_about"),
468
720
  subjectKind: "message",
469
721
  subjectId: messageId,
470
722
  subjectLineageId: messageId,
@@ -476,7 +728,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
476
728
  subjectStateKey: semanticStateKey("message", messageId),
477
729
  objectStateKey: semanticStateKey("reference", referenceLineageId),
478
730
  objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
479
- predicateObjectStateKey: `insight_about#${semanticStateKey("reference", referenceLineageId)}`,
731
+ predicateObjectStateKey: `note_about#${semanticStateKey("reference", referenceLineageId)}`,
480
732
  subjectVersionKey: semanticVersionKey("message", messageId),
481
733
  objectVersionKey: semanticVersionKey("reference", referenceId),
482
734
  score: 1,
@@ -492,17 +744,17 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
492
744
  updatedAt: now,
493
745
  newsroomFeedKey: "semanticRelations",
494
746
  metadata: JSON.stringify({
495
- kind: "reference.insight",
747
+ kind: "reference.note",
496
748
  summary,
497
749
  actor,
498
750
  }),
499
751
  }),
500
- "create insight_about relation",
752
+ "create note_about relation",
501
753
  );
502
754
  await updateNewsroomSummaryForSemanticRelationDelta(
503
755
  client,
504
756
  {
505
- relationTypeKey: "insight_about",
757
+ relationTypeKey: "note_about",
506
758
  relationDomain: "commentary",
507
759
  subjectKind: "message",
508
760
  objectKind: "reference",
@@ -1497,10 +1749,10 @@ function semanticRelationTypeFieldsForPredicate(predicate: string): {
1497
1749
  relationDomain: "curation",
1498
1750
  };
1499
1751
  }
1500
- if (predicate === "insight_about") {
1752
+ if (predicate === "note_about") {
1501
1753
  return {
1502
- relationTypeId: "semantic-relation-type-insight-about",
1503
- relationTypeKey: "insight_about",
1754
+ relationTypeId: "semantic-relation-type-note-about",
1755
+ relationTypeKey: "note_about",
1504
1756
  relationDomain: "knowledge",
1505
1757
  };
1506
1758
  }
@@ -2190,7 +2190,7 @@ fn build_openai_messages(
2190
2190
  ) -> Vec<Value> {
2191
2191
  let mut messages = vec![json!({
2192
2192
  "role": "system",
2193
- "content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable insight, recommend creating an insight Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.edition-candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
2193
+ "content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
2194
2194
  })];
2195
2195
  if !static_prompt.publication_mission.trim().is_empty() {
2196
2196
  messages.push(json!({
@@ -22,6 +22,7 @@ from papyrus_content.rebuild_trigger import (
22
22
  trigger_rebuild,
23
23
  trigger_staging_build,
24
24
  )
25
+ from papyrus_content.story_reporting import save_story_version
25
26
  from papyrus_content.publishing import (
26
27
  ItemFields,
27
28
  PublishError,
@@ -188,6 +189,28 @@ def run_projection_task(task: dict) -> dict:
188
189
 
189
190
  def save_item_draft_action(arguments: dict, actor: str) -> dict:
190
191
  client, _claims = create_authoring_client()
192
+ if arguments.get("newVersion") and arguments.get("id"):
193
+ existing = client.get_record("Item", arguments["id"])
194
+ if existing is None:
195
+ raise PublishError("not-found", "That story was not found.")
196
+ record = save_story_version(
197
+ client,
198
+ existing.get("lineageId") or existing["id"],
199
+ front_matter_yaml=arguments.get("frontMatterYaml"),
200
+ body_markus=arguments.get("bodyMarkus") or "",
201
+ actor=actor,
202
+ change_reason=arguments.get("changeReason") or "edited in the newsroom",
203
+ )
204
+ return {
205
+ "ok": True,
206
+ "item": {
207
+ "id": record["id"],
208
+ "contentHash": record["contentHash"],
209
+ "status": record["status"],
210
+ "slug": record["slug"],
211
+ "versionNumber": record.get("versionNumber"),
212
+ },
213
+ }
191
214
  fields = ItemFields(
192
215
  type=arguments["type"],
193
216
  slug=arguments["slug"],
@@ -1,5 +1,5 @@
1
1
  PyYAML>=6.0.2,<7
2
2
  citeproc-py>=0.8.2,<1
3
- anthus-markus==0.5.1
3
+ anthus-markus==0.7.0
4
4
  Pillow>=11.3,<12
5
5
  awscrt>=0.23,<1
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import { pythonLambdaCode } from "../shared/python-bundle.js";
8
8
  const dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const projectRoot = path.resolve(dirname, "../../..");
10
- export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
10
+ export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
11
11
  function installCheckoutRequirements(outputDir) {
12
12
  const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
13
13
  execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
@@ -10,7 +10,7 @@ import { pythonLambdaCode, type SiteBundleFile } from "../shared/python-bundle";
10
10
  const dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  const projectRoot = path.resolve(dirname, "../../..");
12
12
 
13
- export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
13
+ export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
14
14
 
15
15
  function installCheckoutRequirements(outputDir: string): void {
16
16
  const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
@@ -1,4 +1,4 @@
1
1
  PyYAML>=6.0.2,<7
2
2
  citeproc-py>=0.8.2,<1
3
- anthus-markus==0.5.1
3
+ anthus-markus==0.7.0
4
4
  Pillow>=11.3,<12
@@ -0,0 +1,147 @@
1
+ import {
2
+ EXPIRED_REQUEST_GRANTOR_SUB,
3
+ MAXIMUM_PENDING_DELEGATION_REQUESTS,
4
+ PENDING_DELEGATION_MINUTES,
5
+ PENDING_REQUEST_GRANTOR_SUB,
6
+ delegationExpiry,
7
+ delegationState,
8
+ formatUserCode,
9
+ newUserCode,
10
+ normalizeUserCode,
11
+ type CurationDelegation,
12
+ } from "./curation-delegation"
13
+
14
+ export type DelegationStore = {
15
+ read(delegationId: string): Promise<CurationDelegation | null>
16
+ write(delegation: CurationDelegation, now: string, created: boolean): Promise<void>
17
+ listPending(): Promise<CurationDelegation[]>
18
+ }
19
+
20
+ export type SignedInPerson = { sub: string; label: string; isAdmin: boolean }
21
+
22
+ const DELEGATION_ID_PATTERN = /^curation-delegation-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
23
+ const TOKEN_HASH_PATTERN = /^[0-9a-f]{64}$/
24
+ const MAXIMUM_AGENT_LABEL_LENGTH = 80
25
+ const USER_CODE_ATTEMPTS = 50
26
+
27
+ export function delegationResult(delegation: CurationDelegation, now: string) {
28
+ const state = delegationState(delegation, now)
29
+ return {
30
+ ok: true,
31
+ delegationId: delegation.delegationId,
32
+ token: null,
33
+ agentLabel: delegation.agentLabel,
34
+ grantorLabel: delegation.grantorLabel || null,
35
+ expiresAt: delegation.expiresAt,
36
+ revokedAt: delegation.revokedAt,
37
+ state,
38
+ userCode: state === "pending" && delegation.userCode ? formatUserCode(delegation.userCode) : null,
39
+ requestedHours: delegation.requestedHours ?? null,
40
+ pendingExpiresAt: delegation.pendingExpiresAt ?? null,
41
+ approvedAt: delegation.approvedAt ?? null,
42
+ deniedAt: delegation.deniedAt ?? null,
43
+ }
44
+ }
45
+
46
+ export async function livePendingRequests(store: DelegationStore, now: string): Promise<CurationDelegation[]> {
47
+ const live: CurationDelegation[] = []
48
+ for (const candidate of await store.listPending()) {
49
+ if (delegationState(candidate, now) === "pending") {
50
+ live.push(candidate)
51
+ } else if (candidate.grantorSub === PENDING_REQUEST_GRANTOR_SUB) {
52
+ await store.write({ ...candidate, grantorSub: EXPIRED_REQUEST_GRANTOR_SUB }, now, false)
53
+ }
54
+ }
55
+ return live
56
+ }
57
+
58
+ export async function requestDelegation(
59
+ store: DelegationStore,
60
+ input: { delegationId: string; tokenHash: string; agentLabel: string; hours: number | null | undefined },
61
+ now: string,
62
+ ) {
63
+ if (!DELEGATION_ID_PATTERN.test(input.delegationId)) {
64
+ throw new Error("The delegation id must look like curation-delegation-<uuid>.")
65
+ }
66
+ if (!TOKEN_HASH_PATTERN.test(input.tokenHash)) throw new Error("The token hash must be 64 lowercase hexadecimal characters.")
67
+ const agentLabel = input.agentLabel.trim()
68
+ if (!agentLabel) throw new Error("Name the agent asking for the delegation.")
69
+ if (agentLabel.length > MAXIMUM_AGENT_LABEL_LENGTH) throw new Error("The agent name is too long.")
70
+ const requestedHours = input.hours ?? 8
71
+ delegationExpiry(now, requestedHours)
72
+ if (await store.read(input.delegationId)) throw new Error("That delegation id is already in use.")
73
+ const live = await livePendingRequests(store, now)
74
+ if (live.length >= MAXIMUM_PENDING_DELEGATION_REQUESTS) {
75
+ throw new Error("Too many delegation requests are waiting for approval. Try again later.")
76
+ }
77
+ const takenCodes = new Set(live.map((request) => request.userCode))
78
+ let userCode = newUserCode()
79
+ for (let attempt = 0; takenCodes.has(userCode); attempt += 1) {
80
+ if (attempt >= USER_CODE_ATTEMPTS) throw new Error("Could not allocate a unique approval code. Try again.")
81
+ userCode = newUserCode()
82
+ }
83
+ const pendingExpiresAt = new Date(Date.parse(now) + PENDING_DELEGATION_MINUTES * 60_000).toISOString()
84
+ const delegation: CurationDelegation = {
85
+ delegationId: input.delegationId,
86
+ tokenHash: input.tokenHash,
87
+ grantorSub: PENDING_REQUEST_GRANTOR_SUB,
88
+ grantorLabel: "",
89
+ agentLabel,
90
+ createdAt: now,
91
+ expiresAt: pendingExpiresAt,
92
+ revokedAt: null,
93
+ userCode,
94
+ requestedHours,
95
+ pendingExpiresAt,
96
+ approvedAt: null,
97
+ deniedAt: null,
98
+ }
99
+ await store.write(delegation, now, true)
100
+ return {
101
+ ok: true,
102
+ delegationId: delegation.delegationId,
103
+ userCode: formatUserCode(userCode),
104
+ pendingExpiresAt,
105
+ requestedHours,
106
+ }
107
+ }
108
+
109
+ async function findLivePendingByCode(store: DelegationStore, typedCode: string, now: string): Promise<CurationDelegation> {
110
+ const code = normalizeUserCode(typedCode)
111
+ const found = code ? (await livePendingRequests(store, now)).find((request) => request.userCode === code) : undefined
112
+ if (!found) throw new Error("No pending delegation request has that code. It may have expired or already been decided.")
113
+ return found
114
+ }
115
+
116
+ export async function approveDelegation(store: DelegationStore, person: SignedInPerson, typedCode: string, now: string) {
117
+ const pending = await findLivePendingByCode(store, typedCode, now)
118
+ const approved: CurationDelegation = {
119
+ ...pending,
120
+ grantorSub: person.sub,
121
+ grantorLabel: person.label,
122
+ createdAt: now,
123
+ expiresAt: delegationExpiry(now, pending.requestedHours),
124
+ approvedAt: now,
125
+ userCode: null,
126
+ }
127
+ await store.write(approved, now, false)
128
+ return delegationResult(approved, now)
129
+ }
130
+
131
+ export async function denyDelegation(store: DelegationStore, person: SignedInPerson, typedCode: string, now: string) {
132
+ const pending = await findLivePendingByCode(store, typedCode, now)
133
+ const denied: CurationDelegation = { ...pending, grantorSub: person.sub, grantorLabel: person.label, deniedAt: now, userCode: null }
134
+ await store.write(denied, now, false)
135
+ return delegationResult(denied, now)
136
+ }
137
+
138
+ export async function delegationStatus(store: DelegationStore, delegationId: string, now: string) {
139
+ const delegation = await store.read(delegationId)
140
+ if (!delegation) throw new Error("That delegation was not found.")
141
+ return {
142
+ delegationId: delegation.delegationId,
143
+ state: delegationState(delegation, now),
144
+ expiresAt: delegation.expiresAt,
145
+ grantorLabel: delegation.grantorLabel || null,
146
+ }
147
+ }