@anthusai/papyrus 5.43.0-next.47 → 5.43.0-next.48

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.
@@ -247,8 +247,6 @@ const schema = a.schema({
247
247
  referenceId: a.id().required(),
248
248
  status: a.string().required(),
249
249
  reasonCode: a.string(),
250
- messageId: a.id(),
251
- relationId: a.id(),
252
250
  }),
253
251
  ReferenceQualityActionResult: a.customType({
254
252
  ok: a.boolean().required(),
@@ -1569,6 +1567,12 @@ const schema = a.schema({
1569
1567
  curationStatusUpdatedAt: a.datetime(),
1570
1568
  curationStatusUpdatedBy: a.string(),
1571
1569
  curationStatusReason: a.string(),
1570
+ curationReasonCode: a.string(),
1571
+ curationActorKind: a.string(),
1572
+ curationSectionId: a.string(),
1573
+ curationShareable: a.boolean(),
1574
+ curationDelegation: a.json(),
1575
+ curationDecisionRelationId: a.string(),
1572
1576
  newsroomFeedKey: a.string(),
1573
1577
  reviewedFeedKey: a.string(),
1574
1578
  // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
@@ -267,8 +267,6 @@ const schema = a.schema({
267
267
  referenceId: a.id().required(),
268
268
  status: a.string().required(),
269
269
  reasonCode: a.string(),
270
- messageId: a.id(),
271
- relationId: a.id(),
272
270
  }),
273
271
 
274
272
  ReferenceQualityActionResult: a.customType({
@@ -1682,6 +1680,12 @@ const schema = a.schema({
1682
1680
  curationStatusUpdatedAt: a.datetime(),
1683
1681
  curationStatusUpdatedBy: a.string(),
1684
1682
  curationStatusReason: a.string(),
1683
+ curationReasonCode: a.string(),
1684
+ curationActorKind: a.string(),
1685
+ curationSectionId: a.string(),
1686
+ curationShareable: a.boolean(),
1687
+ curationDelegation: a.json(),
1688
+ curationDecisionRelationId: a.string(),
1685
1689
  newsroomFeedKey: a.string(),
1686
1690
  reviewedFeedKey: a.string(),
1687
1691
  // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
@@ -259,8 +259,6 @@ function buildBaseDefinitions(includeEditions) {
259
259
  referenceId: a.id().required(),
260
260
  status: a.string().required(),
261
261
  reasonCode: a.string(),
262
- messageId: a.id(),
263
- relationId: a.id(),
264
262
  }),
265
263
  ReferenceQualityActionResult: a.customType({
266
264
  ok: a.boolean().required(),
@@ -1475,6 +1473,12 @@ function buildBaseDefinitions(includeEditions) {
1475
1473
  curationStatusUpdatedAt: a.datetime(),
1476
1474
  curationStatusUpdatedBy: a.string(),
1477
1475
  curationStatusReason: a.string(),
1476
+ curationReasonCode: a.string(),
1477
+ curationActorKind: a.string(),
1478
+ curationSectionId: a.string(),
1479
+ curationShareable: a.boolean(),
1480
+ curationDelegation: a.json(),
1481
+ curationDecisionRelationId: a.string(),
1478
1482
  newsroomFeedKey: a.string(),
1479
1483
  reviewedFeedKey: a.string(),
1480
1484
  // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
@@ -285,8 +285,6 @@ const definitions = {
285
285
  referenceId: a.id().required(),
286
286
  status: a.string().required(),
287
287
  reasonCode: a.string(),
288
- messageId: a.id(),
289
- relationId: a.id(),
290
288
  }),
291
289
 
292
290
  ReferenceQualityActionResult: a.customType({
@@ -1586,6 +1584,12 @@ const definitions = {
1586
1584
  curationStatusUpdatedAt: a.datetime(),
1587
1585
  curationStatusUpdatedBy: a.string(),
1588
1586
  curationStatusReason: a.string(),
1587
+ curationReasonCode: a.string(),
1588
+ curationActorKind: a.string(),
1589
+ curationSectionId: a.string(),
1590
+ curationShareable: a.boolean(),
1591
+ curationDelegation: a.json(),
1592
+ curationDecisionRelationId: a.string(),
1589
1593
  newsroomFeedKey: a.string(),
1590
1594
  reviewedFeedKey: a.string(),
1591
1595
  // The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
@@ -4,7 +4,7 @@ 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 { curationActorKind, referenceCurationMetadata, validatedCurationSectionId } from "../shared/reference-curation-metadata";
7
+ import { curationActorKind, referenceVoteFields, validatedCurationSectionId } from "../shared/reference-curation-metadata";
8
8
  import {
9
9
  CURATION_DELEGATION_OWNER_TYPE,
10
10
  CURATION_DELEGATION_PAYLOAD_KIND,
@@ -236,17 +236,10 @@ async function reviewReferenceCuration(
236
236
  const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
237
237
  const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
238
238
  const note = writtenReason;
239
- const referenceLineageId = normalizeOptionalString(reference.lineageId) ?? referenceId;
240
- const referenceVersionNumber = typeof reference.versionNumber === "number" ? reference.versionNumber : null;
241
- const referenceTitle = normalizeOptionalString(reference.title) ?? normalizeOptionalString(reference.externalItemId) ?? referenceId;
242
- const messageId = `message-reference-curation-${safeId(referenceLineageId)}-${safeId(action)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
243
- const relationId = `semantic-relation-${hashStable([
244
- `message#${messageId}`,
245
- "comment",
246
- `reference#${referenceId}`,
247
- action,
248
- ]).slice(0, 16)}`;
239
+ const decisionRelationId = normalizeOptionalString(event.arguments.decisionRelationId);
249
240
 
241
+ // The vote is recorded on the Reference itself: its status, who set it and the note, plus the details of the vote.
242
+ // Messages come only from people, so a vote writes no Message and no comment relation.
250
243
  await requireDataResult(
251
244
  client.models.Reference.update({
252
245
  id: referenceId,
@@ -255,6 +248,14 @@ async function reviewReferenceCuration(
255
248
  curationStatusUpdatedAt: now,
256
249
  curationStatusUpdatedBy: actor,
257
250
  curationStatusReason: note,
251
+ ...referenceVoteFields({
252
+ actorKind: delegation ? "human" : curationActorKind(event.identity),
253
+ reasonCode,
254
+ sectionId,
255
+ shareable: event.arguments.shareable ?? null,
256
+ delegation,
257
+ decisionRelationId,
258
+ }),
258
259
  newsroomFeedKey: reference.newsroomFeedKey ?? "references",
259
260
  reviewedFeedKey: reviewedFeedKeyForStatus(nextStatus),
260
261
  // A vote moves the Reference between the unrated and rated feeds; its original prediction and arrival stay as they were.
@@ -264,91 +265,6 @@ async function reviewReferenceCuration(
264
265
  "update Reference curation status",
265
266
  );
266
267
 
267
- await requireDataResult(
268
- client.models.Message.create(buildCanonicalMessageInput({
269
- id: messageId,
270
- messageKind: "reference_curation",
271
- messageDomain: "commentary",
272
- summary: `${referenceTitle}: ${nextStatus}`,
273
- source: "newsroom",
274
- authorSub: actorSub,
275
- authorLabel: actor,
276
- now,
277
- })),
278
- "create Message",
279
- );
280
- await putTextModelPayload(
281
- client as any,
282
- { ownerKind: "message", ownerId: messageId, ownerLineageId: messageId },
283
- "message_body",
284
- "message",
285
- note ?? `${actor} marked this reference ${nextStatus}.`,
286
- { filename: "message.txt", now },
287
- );
288
- await putJsonModelPayload(
289
- client as any,
290
- { ownerKind: "message", ownerId: messageId, ownerLineageId: messageId },
291
- "metadata",
292
- "metadata",
293
- referenceCurationMetadata({
294
- action,
295
- curationStatus: nextStatus,
296
- reasonCode,
297
- referenceId,
298
- referenceLineageId,
299
- decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
300
- shareable: event.arguments.shareable ?? null,
301
- actorKind: delegation ? "human" : curationActorKind(event.identity),
302
- sectionId,
303
- delegation,
304
- }),
305
- { filename: "metadata.json", now },
306
- );
307
-
308
- await requireDataResult(
309
- client.models.SemanticRelation.create({
310
- id: relationId,
311
- relationState: "current",
312
- predicate: "comment",
313
- relationTypeId: "semantic-relation-type-comment",
314
- relationTypeKey: "comment",
315
- relationDomain: "commentary",
316
- subjectKind: "message",
317
- subjectId: messageId,
318
- subjectLineageId: messageId,
319
- subjectVersionNumber: 1,
320
- objectKind: "reference",
321
- objectId: referenceId,
322
- objectLineageId: referenceLineageId,
323
- objectVersionNumber: referenceVersionNumber,
324
- subjectStateKey: semanticStateKey("message", messageId),
325
- objectStateKey: semanticStateKey("reference", referenceLineageId),
326
- objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
327
- predicateObjectStateKey: `comment#${semanticStateKey("reference", referenceLineageId)}`,
328
- subjectVersionKey: semanticVersionKey("message", messageId),
329
- objectVersionKey: semanticVersionKey("reference", referenceId),
330
- score: 1,
331
- confidence: null,
332
- rank: 1,
333
- classifierId: null,
334
- modelVersion: null,
335
- reviewRecommended: false,
336
- sourceSnapshotId: null,
337
- importRunId: null,
338
- importedAt: now,
339
- createdAt: now,
340
- updatedAt: now,
341
- newsroomFeedKey: "semanticRelations",
342
- metadata: JSON.stringify({
343
- action,
344
- curationStatus: nextStatus,
345
- reasonCode,
346
- messageKind: "reference_curation",
347
- }),
348
- }),
349
- "create Message SemanticRelation",
350
- );
351
-
352
268
  await updateNewsroomSummaryForReferenceCuration(client, {
353
269
  corpusId: normalizeOptionalString(reference.corpusId),
354
270
  previousStatus: normalizeOptionalString(reference.curationStatus) ?? "pending",
@@ -362,8 +278,6 @@ async function reviewReferenceCuration(
362
278
  referenceId,
363
279
  status: nextStatus,
364
280
  reasonCode,
365
- messageId,
366
- relationId,
367
281
  };
368
282
  }
369
283
 
@@ -861,6 +775,12 @@ async function moveReferenceCorpus(event: Parameters<MoveReferenceCorpusHandler>
861
775
  curationStatusUpdatedAt: normalizeOptionalString(current.curationStatusUpdatedAt) ?? now,
862
776
  curationStatusUpdatedBy: normalizeOptionalString(current.curationStatusUpdatedBy) ?? actor,
863
777
  curationStatusReason: normalizeOptionalString(current.curationStatusReason),
778
+ curationReasonCode: normalizeOptionalString(current.curationReasonCode),
779
+ curationActorKind: normalizeOptionalString(current.curationActorKind),
780
+ curationSectionId: normalizeOptionalString(current.curationSectionId),
781
+ curationShareable: typeof current.curationShareable === "boolean" ? current.curationShareable : null,
782
+ curationDelegation: current.curationDelegation ?? null,
783
+ curationDecisionRelationId: normalizeOptionalString(current.curationDecisionRelationId),
864
784
  newsroomFeedKey: normalizeOptionalString(current.newsroomFeedKey) ?? "references",
865
785
  reviewedFeedKey: normalizeOptionalString(current.reviewedFeedKey) ?? reviewedFeedKeyForStatus(curationStatus),
866
786
  predictedAcceptProbability: typeof current.predictedAcceptProbability === "number" ? current.predictedAcceptProbability : null,
@@ -2188,9 +2108,6 @@ async function updateNewsroomSummaryForReferenceCuration(
2188
2108
  payload.generatedAt = input.now;
2189
2109
  payload.staleAt = new Date(Date.parse(input.now) + SUMMARY_STALE_AFTER_MS).toISOString();
2190
2110
  payload.source = "incremental";
2191
- payload.counts.messages = Math.max(0, (payload.counts.messages ?? 0) + 1);
2192
- payload.counts.modelAttachments = Math.max(0, (payload.counts.modelAttachments ?? 0) + 2);
2193
- payload.counts.semanticRelations = Math.max(0, (payload.counts.semanticRelations ?? 0) + 1);
2194
2111
  if (input.previousStatus !== input.nextStatus) {
2195
2112
  increment(payload.referenceStatusCounts, input.previousStatus, -1);
2196
2113
  increment(payload.referenceStatusCounts, input.nextStatus, 1);
@@ -2202,23 +2119,6 @@ async function updateNewsroomSummaryForReferenceCuration(
2202
2119
  increment(payload.facets.references.statusByCorpus[input.corpusId], input.nextStatus, 1);
2203
2120
  }
2204
2121
  }
2205
- increment(payload.messageKindCounts, "reference_curation", 1);
2206
- increment(payload.messageDomainCounts, "commentary", 1);
2207
- increment(payload.facets.messages.byKind, "reference_curation", 1);
2208
- increment(payload.facets.messages.byDomain, "commentary", 1);
2209
- increment(payload.facets.messages.byStatus, "active", 1);
2210
- if (!payload.facets.messages.domainByKind.reference_curation) payload.facets.messages.domainByKind.reference_curation = {};
2211
- increment(payload.facets.messages.domainByKind.reference_curation, "commentary", 1);
2212
- increment(payload.facets.modelAttachments.byOwnerKind, "message", 2);
2213
- increment(payload.facets.modelAttachments.byRole, "message_body", 1);
2214
- increment(payload.facets.modelAttachments.byRole, "metadata", 1);
2215
- increment(payload.facets.modelAttachments.byMediaType, "text/plain", 1);
2216
- increment(payload.facets.modelAttachments.byMediaType, "application/json", 1);
2217
- increment(payload.facets.modelAttachments.byStatus, "active", 2);
2218
- increment(payload.facets.semanticRelations.byRelationTypeKey, "comment", 1);
2219
- increment(payload.facets.semanticRelations.byRelationDomain, "workflow", 1);
2220
- increment(payload.facets.semanticRelations.bySubjectKind, "message", 1);
2221
- increment(payload.facets.semanticRelations.byObjectKind, "reference", 1);
2222
2122
  await upsertNewsroomSummaryPayload(client, payload, response.data, input.now);
2223
2123
  }
2224
2124
 
@@ -1,23 +1,19 @@
1
1
  /**
2
- * Metadata stored with an editor's reference curation Message. When the
3
- * relevance cyclotron decided the reference, the review records which
4
- * decision it answered and whether its explanation may be shared (for the
5
- * Cyclotron marketing recording). Both are optional.
2
+ * The details of a vote, kept on the Reference itself (Messages come only from people).
3
+ * When the relevance cyclotron decided the reference, the vote records which decision
4
+ * it answered and whether its explanation may be shared (for the Cyclotron marketing
5
+ * recording). Both are optional. A Reference keeps only its latest vote.
6
6
  */
7
- export type ReferenceCurationMetadataInput = {
8
- action: string
9
- curationStatus: string
10
- reasonCode: string | null
11
- referenceId: string
12
- referenceLineageId: string
7
+ export type ReferenceVoteInput = {
8
+ actorKind: CurationActorKind
9
+ reasonCode?: string | null
10
+ sectionId?: string | null
13
11
  decisionRelationId?: string | null
14
12
  shareable?: boolean | null
15
- actorKind?: CurationActorKind | null
16
- sectionId?: string | null
17
13
  delegation?: { delegationId: string; relayedBy: string; instruction: string } | null
18
14
  }
19
15
 
20
- export type CurationActorKind = "human" | "service"
16
+ export type CurationActorKind = "human" | "agent" | "cyclotron" | "rule" | "catalog"
21
17
 
22
18
  /**
23
19
  * A curation is human only when the caller signed in through the user pool, so
@@ -27,26 +23,22 @@ export type CurationActorKind = "human" | "service"
27
23
  export function curationActorKind(identity: unknown): CurationActorKind {
28
24
  const claims = (identity as {claims?: Record<string, unknown>} | null | undefined)?.claims
29
25
  const subject = claims?.sub
30
- return typeof subject === "string" && subject.trim() !== "" ? "human" : "service"
26
+ return typeof subject === "string" && subject.trim() !== "" ? "human" : "agent"
31
27
  }
32
28
 
33
- export function referenceCurationMetadata(input: ReferenceCurationMetadataInput): Record<string, unknown> {
29
+ /**
30
+ * The Reference fields a vote sets. Every field is always present, so a new vote clears
31
+ * what the last one carried. The delegation is stored as JSON.
32
+ */
33
+ export function referenceVoteFields(input: ReferenceVoteInput): Record<string, unknown> {
34
34
  const decisionRelationId = input.decisionRelationId?.trim() || null
35
35
  return {
36
- action: input.action,
37
- curationStatus: input.curationStatus,
38
- reasonCode: input.reasonCode,
39
- curationReasonCode: input.reasonCode,
40
- referenceId: input.referenceId,
41
- referenceLineageId: input.referenceLineageId,
42
- ...(input.actorKind ? {actorKind: input.actorKind} : {}),
43
- ...(input.sectionId?.trim() ? {sectionId: input.sectionId.trim()} : {}),
44
- ...(input.delegation
45
- ? {attestation: "delegated", relayedBy: input.delegation.relayedBy, delegationId: input.delegation.delegationId,
46
- instruction: input.delegation.instruction}
47
- : {}),
48
- ...(decisionRelationId ? {decisionRelationId} : {}),
49
- ...(decisionRelationId ? {shareable: input.shareable === true} : {}),
36
+ curationActorKind: input.actorKind,
37
+ curationReasonCode: input.reasonCode?.trim() || null,
38
+ curationSectionId: input.sectionId?.trim() || null,
39
+ curationShareable: decisionRelationId ? input.shareable === true : null,
40
+ curationDelegation: input.delegation ? JSON.stringify(input.delegation) : null,
41
+ curationDecisionRelationId: decisionRelationId,
50
42
  }
51
43
  }
52
44
 
@@ -302,7 +302,7 @@ const NEWSROOM_ASSIGNMENT_FEED_QUERY = `
302
302
  const NEWSROOM_REFERENCE_FEED_QUERY = `
303
303
  query ListReferencesByNewsroomFeedAndCreatedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
304
304
  listReferencesByNewsroomFeedAndCreatedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
305
- items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
305
+ items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey updatedAt }
306
306
  nextToken
307
307
  }
308
308
  }
@@ -311,7 +311,7 @@ const NEWSROOM_REFERENCE_FEED_QUERY = `
311
311
  const NEWSROOM_REFERENCE_IMPORTED_FEED_QUERY = `
312
312
  query ListReferencesByNewsroomFeedAndImportedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
313
313
  listReferencesByNewsroomFeedAndImportedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
314
- items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
314
+ items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey updatedAt }
315
315
  nextToken
316
316
  }
317
317
  }
@@ -320,7 +320,7 @@ const NEWSROOM_REFERENCE_IMPORTED_FEED_QUERY = `
320
320
  const LIST_REFERENCES_BY_CURATION_STATUS_AND_UPDATED_AT_QUERY = `
321
321
  query ListReferencesByCurationStatusAndUpdatedAt($curationStatus: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
322
322
  listReferencesByCurationStatusAndUpdatedAt(curationStatus: $curationStatus, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
323
- items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
323
+ items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey updatedAt }
324
324
  nextToken
325
325
  }
326
326
  }
@@ -329,7 +329,7 @@ const LIST_REFERENCES_BY_CURATION_STATUS_AND_UPDATED_AT_QUERY = `
329
329
  const LIST_REFERENCES_BY_REVIEWED_FEED_AND_UPDATED_AT_QUERY = `
330
330
  query ListReferencesByReviewedFeedAndUpdatedAt($reviewedFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelReferenceFilterInput) {
331
331
  listReferencesByReviewedFeedAndUpdatedAt(reviewedFeedKey: $reviewedFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
332
- items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
332
+ items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey updatedAt }
333
333
  nextToken
334
334
  }
335
335
  }
@@ -338,7 +338,7 @@ const LIST_REFERENCES_BY_REVIEWED_FEED_AND_UPDATED_AT_QUERY = `
338
338
  const LIST_REFERENCES_FALLBACK_QUERY = `
339
339
  query ListReferencesFallback($limit: Int, $nextToken: String) {
340
340
  listReferences(limit: $limit, nextToken: $nextToken) {
341
- items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt }
341
+ items { id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey updatedAt }
342
342
  nextToken
343
343
  }
344
344
  }
@@ -471,7 +471,7 @@ const MODEL_ATTACHMENTS_BY_OWNER_QUERY = `
471
471
 
472
472
  /** Matches Reference model fields in amplify/data/resource.ts (no metadata on Reference). */
473
473
  const REFERENCE_RECORD_GRAPHQL_FIELDS =
474
- "id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason newsroomFeedKey reviewedFeedKey updatedAt";
474
+ "id lineageId versionNumber previousVersionId versionState versionCreatedAt versionCreatedBy changeReason contentHash corpusId externalItemId title authors sourceUri storagePath mediaType byteSize sha256 sourcePublishedAt sourceUpdatedAt retrievedAt inboundCitationCount outboundCitationCount importRunId importedAt createdAt curationStatus curationStatusKey curationStatusUpdatedAt curationStatusUpdatedBy curationStatusReason curationReasonCode curationActorKind curationSectionId curationShareable curationDelegation curationDecisionRelationId newsroomFeedKey reviewedFeedKey updatedAt";
475
475
 
476
476
  const LIST_REFERENCES_BY_LINEAGE_AND_VERSION_QUERY = `
477
477
  query ListReferencesByLineageAndVersion($lineageId: ID!, $limit: Int, $nextToken: String) {
@@ -2153,17 +2153,15 @@ export async function loadEditorReferenceReviewDetails(lineageId: string) {
2153
2153
  configureAmplifyClient();
2154
2154
  const testMock = getTestEditorNewsroomMock();
2155
2155
  return loadReferenceReviewDetails(lineageId, {
2156
- relations: async (direction) => {
2156
+ relations: async () => {
2157
2157
  if (testMock?.semanticRelations) return testMock.semanticRelations;
2158
2158
  return loadSemanticRelationsByState({
2159
- field: direction === "outgoing" ? "listSemanticRelationsBySubjectState" : "listSemanticRelationsByObjectState",
2160
- keyName: direction === "outgoing" ? "subjectStateKey" : "objectStateKey",
2159
+ field: "listSemanticRelationsBySubjectState",
2160
+ keyName: "subjectStateKey",
2161
2161
  keyValue: `reference#${lineageId}#current`,
2162
- query: direction === "outgoing" ? LIST_SEMANTIC_RELATIONS_BY_SUBJECT_STATE_QUERY : LIST_SEMANTIC_RELATIONS_BY_OBJECT_STATE_QUERY,
2162
+ query: LIST_SEMANTIC_RELATIONS_BY_SUBJECT_STATE_QUERY,
2163
2163
  });
2164
2164
  },
2165
- message: async (id) => testMock?.messages ? testMock.messages.find((message) => message.id === id) ?? null : getMessageRecordById(id),
2166
- payloads: (id) => loadModelPayloadsForOwner("message", id, ["metadata"]),
2167
2165
  });
2168
2166
  }
2169
2167
 
@@ -776,17 +776,22 @@ function NewsDeskDashboard({
776
776
  () => (isLocalPreviewEnabled() && !semanticRelations.some((relation) => relation.predicate === "relevance_decision_is") ? createLocalPreviewCyclotronFixtures(references) : null),
777
777
  [references, semanticRelations],
778
778
  );
779
+ // A Reference keeps its latest vote on itself; the preview fixtures overlay votes on the first few.
780
+ const votedReferences = useMemo(
781
+ () => (cyclotronPreviewFixtures ? references.map((reference) => cyclotronPreviewFixtures.votedReferences.get(reference.id) ?? reference) : references),
782
+ [cyclotronPreviewFixtures, references],
783
+ );
779
784
  const relevanceDecisions = useMemo(
780
785
  () => currentRelevanceDecisions(cyclotronPreviewFixtures ? [...semanticRelations, ...cyclotronPreviewFixtures.relations] : semanticRelations),
781
786
  [cyclotronPreviewFixtures, semanticRelations],
782
787
  );
783
788
  const delegatedCurations = useMemo(
784
- () => delegatedCurationsByReference(cyclotronPreviewFixtures ? [...messages, ...cyclotronPreviewFixtures.messages] : messages),
785
- [cyclotronPreviewFixtures, messages],
789
+ () => delegatedCurationsByReference(votedReferences),
790
+ [votedReferences],
786
791
  );
787
792
  const humanCuratedReferenceLineageIds = useMemo(
788
- () => referencesWithHumanLatestCuration(cyclotronPreviewFixtures ? [...messages, ...cyclotronPreviewFixtures.messages] : messages),
789
- [cyclotronPreviewFixtures, messages],
793
+ () => referencesWithHumanLatestCuration(votedReferences),
794
+ [votedReferences],
790
795
  );
791
796
  const relevanceCyclotronId = useMemo(
792
797
  () => [...relevanceDecisions.values()].find((decision) => decision.cyclotronId)?.cyclotronId ?? null,
@@ -905,8 +910,8 @@ function NewsDeskDashboard({
905
910
  const referenceReviewsPending = useRef(new Map<string, Promise<void>>());
906
911
  const [selectedReviewRead, setSelectedReviewRead] = useState<{ lineageId: string; state: "loading" | "ready" | "error"; error: string | null }>({ lineageId: "", state: "ready", error: null });
907
912
  const [selectedReviewAttempt, setSelectedReviewAttempt] = useState(0);
908
- const reviewReasons = useMemo(() => latestReferenceReviewReasons(messages), [messages]);
909
- const reviewSectionIds = useMemo(() => latestReferenceReviewSections(messages), [messages]);
913
+ const reviewReasons = useMemo(() => latestReferenceReviewReasons(references), [references]);
914
+ const reviewSectionIds = useMemo(() => latestReferenceReviewSections(references), [references]);
910
915
 
911
916
  useEffect(() => {
912
917
  referencesRef.current = references;
@@ -1331,10 +1336,9 @@ function NewsDeskDashboard({
1331
1336
  if (!canLoadPrivateCorpus || activeTab !== "references" || !selectedReviewLineageId) return;
1332
1337
  let active = true;
1333
1338
  setSelectedReviewRead({ lineageId: selectedReviewLineageId, state: "loading", error: null });
1334
- void loadEditorReferenceReviewDetails(selectedReviewLineageId).then(({ relations, messages: history }) => {
1339
+ void loadEditorReferenceReviewDetails(selectedReviewLineageId).then(({ relations }) => {
1335
1340
  if (!active) return;
1336
1341
  setSemanticRelations((current) => [...current.filter((relation) => relation.subjectLineageId !== selectedReviewLineageId || relation.subjectKind !== "reference"), ...relations]);
1337
- setMessages((current) => [...current.filter((message) => !history.some((entry) => entry.id === message.id)), ...history]);
1338
1342
  setSelectedReviewRead({ lineageId: selectedReviewLineageId, state: "ready", error: null });
1339
1343
  }).catch((error) => {
1340
1344
  if (active) setSelectedReviewRead({ lineageId: selectedReviewLineageId, state: "error", error: error instanceof Error ? error.message : "Reference review details could not be loaded." });
@@ -1621,59 +1625,18 @@ function NewsDeskDashboard({
1621
1625
  setActionState({ id: reference.id, message: `${action} pending`, tone: "pending" });
1622
1626
  if (dashboard.isDemo) {
1623
1627
  const now = new Date().toISOString();
1624
- const messageId = `message-demo-${reference.id}-${action}-${now.replace(/[^0-9]/g, "")}`;
1625
- const relationId = `semantic-relation-demo-${messageId}`;
1626
1628
  applyReferenceRecord(buildReviewedReferenceRecord(referencesRef.current, reference, {
1627
1629
  actorLabel: authState.label,
1628
1630
  nextStatus,
1629
1631
  note: note ?? null,
1630
1632
  now,
1631
- }));
1632
- setMessages((current) => [{
1633
- id: messageId,
1634
- messageKind: "reference_curation",
1635
- messageDomain: "commentary",
1636
- status: "active",
1637
- body: note?.trim() || `${authState.label} marked this reference ${nextStatus}.`,
1638
- summary: `${reference.title ?? reference.externalItemId}: ${nextStatus}`,
1639
- source: "newsroom",
1640
- authorLabel: authState.label,
1641
- createdAt: now,
1642
- updatedAt: now,
1643
- metadata: {
1644
- referenceLineageId: reference.lineageId ?? reference.id,
1645
- action,
1646
- curationStatus: nextStatus,
1633
+ vote: {
1647
1634
  reasonCode: action === "reject" ? reasonCode ?? null : null,
1648
- ...(relevance ? { decisionRelationId: relevance.decisionRelationId, shareable: relevance.shareable } : {}),
1649
- ...(action === "accept" && sectionId ? { sectionId } : {}),
1635
+ sectionId: action === "accept" ? sectionId ?? null : null,
1636
+ decisionRelationId: relevance?.decisionRelationId ?? null,
1637
+ shareable: relevance ? relevance.shareable : null,
1650
1638
  },
1651
- }, ...current]);
1652
- setSemanticRelations((current) => [{
1653
- id: relationId,
1654
- relationState: "current",
1655
- predicate: "comment",
1656
- relationTypeId: "semantic-relation-type-comment",
1657
- relationTypeKey: "comment",
1658
- relationDomain: "commentary",
1659
- subjectKind: "message",
1660
- subjectId: messageId,
1661
- subjectLineageId: messageId,
1662
- subjectVersionNumber: 1,
1663
- objectKind: "reference",
1664
- objectId: reference.id,
1665
- objectLineageId: reference.lineageId ?? reference.id,
1666
- objectVersionNumber: reference.versionNumber ?? null,
1667
- subjectStateKey: `message#${messageId}#current`,
1668
- objectStateKey: `reference#${reference.lineageId ?? reference.id}#current`,
1669
- objectSubjectStateKey: `reference#${reference.lineageId ?? reference.id}#current#message`,
1670
- predicateObjectStateKey: `comment#reference#${reference.lineageId ?? reference.id}#current`,
1671
- subjectVersionKey: `message#${messageId}`,
1672
- objectVersionKey: `reference#${reference.id}`,
1673
- rank: 1,
1674
- reviewRecommended: false,
1675
- importedAt: now,
1676
- }, ...current]);
1639
+ }));
1677
1640
  setActionState({ id: reference.id, message: `${action} saved`, tone: "ok" });
1678
1641
  return;
1679
1642
  }
@@ -1699,6 +1662,12 @@ function NewsDeskDashboard({
1699
1662
  nextStatus: status,
1700
1663
  note: note?.trim() || null,
1701
1664
  now,
1665
+ vote: {
1666
+ reasonCode: action === "reject" ? reasonCode ?? null : null,
1667
+ sectionId: action === "accept" ? sectionId ?? null : null,
1668
+ decisionRelationId: relevance?.decisionRelationId ?? null,
1669
+ shareable: relevance ? relevance.shareable : null,
1670
+ },
1702
1671
  }));
1703
1672
  setActionState({ id: reference.id, message: `${action} saved`, tone: "ok" });
1704
1673
  retryReferenceReviewData();
@@ -8445,11 +8414,7 @@ function SemanticDetailPanel({
8445
8414
  {messages.slice(0, 4).map((message) => (
8446
8415
  <div className="news-desk-detail-line" key={message.id}>
8447
8416
  <span>{message.messageKind}</span>
8448
- <strong>
8449
- {message.messageKind === "reference_curation"
8450
- ? "Canonical summary is stored on the linked reference metadata attachment."
8451
- : (message.summary ?? "Stored message payload")}
8452
- </strong>
8417
+ <strong>{message.summary ?? "Stored message payload"}</strong>
8453
8418
  </div>
8454
8419
  ))}
8455
8420
  </div>
@@ -9858,11 +9823,7 @@ function ReferenceDetailPanel({
9858
9823
  {messages.slice(0, 4).map((message) => (
9859
9824
  <div className="news-desk-detail-line" key={message.id}>
9860
9825
  <span>{message.messageKind}</span>
9861
- <strong>
9862
- {message.messageKind === "reference_curation"
9863
- ? (metadataSummary ?? "Canonical summary unavailable")
9864
- : (message.summary ?? "Stored message payload")}
9865
- </strong>
9826
+ <strong>{message.summary ?? "Stored message payload"}</strong>
9866
9827
  </div>
9867
9828
  ))}
9868
9829
  </div>
@@ -9964,25 +9925,9 @@ function MessageDetailPanel({
9964
9925
  const bodyPayload = modelPayloadByRole(messagePayloadState.payloads, "message_body");
9965
9926
  const metadataPayload = modelPayloadByRole(messagePayloadState.payloads, "metadata");
9966
9927
  const messageMetadata = metadataRecord(metadataPayload?.json) ?? metadataRecord(message.metadata);
9967
- const linkedReference = useReferenceCurationMessageReference(graph, message, messageMetadata);
9968
- const referencePayloadState = useModelPayloads("reference", linkedReference?.id, ["metadata"]);
9969
- const referenceMetadataPayload = modelPayloadByRole(referencePayloadState.payloads, "metadata");
9970
- const referenceSubtitle = referenceMetadataSubtitle(referenceMetadataPayload, linkedReference?.metadata);
9971
- const referenceSummary = linkedReference
9972
- ? referenceDisplaySummary(
9973
- graph,
9974
- linkedReference.lineageId ?? linkedReference.id,
9975
- referenceMetadataPayload,
9976
- linkedReference.metadata,
9977
- )
9978
- : null;
9979
9928
  const headerLabel = humanizeNewsroomLabel(message.messageKind ?? "message");
9980
- const headerTitle = message.messageKind === "reference_curation"
9981
- ? linkedReference?.title ?? linkedReference?.externalItemId ?? "Reference curation"
9982
- : message.summary ?? "Stored message payload";
9983
- const detailSummary = message.messageKind === "reference_curation"
9984
- ? referenceSummary ?? null
9985
- : message.summary ?? null;
9929
+ const headerTitle = message.summary ?? "Stored message payload";
9930
+ const detailSummary = message.summary ?? null;
9986
9931
  const neighborGroups = selected ? graph.neighbors(selected.kind, selected.lineageId) : [];
9987
9932
  return (
9988
9933
  <section className="category-steering-section" aria-label="Message detail" data-news-desk-message-detail={message.id}>
@@ -9991,9 +9936,6 @@ function MessageDetailPanel({
9991
9936
  <div>
9992
9937
  <p className="story-label">{headerLabel}</p>
9993
9938
  <strong data-news-desk-message-headline>{headerTitle}</strong>
9994
- {message.messageKind === "reference_curation" && referenceSubtitle ? (
9995
- <p className="news-desk-semantic-detail__subheading" data-news-desk-message-subheading>{referenceSubtitle}</p>
9996
- ) : null}
9997
9939
  <span>{formatDateTime(message.createdAt)}</span>
9998
9940
  </div>
9999
9941
  </header>
@@ -10012,14 +9954,13 @@ function MessageDetailPanel({
10012
9954
  <p className="story-label">Message Metadata</p>
10013
9955
  {detailSummary ? (
10014
9956
  <div className="news-desk-detail-line" data-news-desk-message-summary>
10015
- <span>{message.messageKind === "reference_curation" ? "Summary" : "Event summary"}</span>
9957
+ <span>Event summary</span>
10016
9958
  <strong>{detailSummary}</strong>
10017
9959
  </div>
10018
9960
  ) : null}
10019
9961
  <div className="news-desk-detail-line"><span>Status</span><strong>{message.status}</strong></div>
10020
9962
  <div className="news-desk-detail-line"><span>Source</span><strong>{message.source ?? "unknown"}</strong></div>
10021
9963
  <div className="news-desk-detail-line"><span>Author</span><strong>{message.authorLabel ?? message.authorSub ?? "unknown"}</strong></div>
10022
- {linkedReference ? <div className="news-desk-detail-line"><span>Reference</span><strong>{linkedReference.title ?? linkedReference.externalItemId}</strong></div> : null}
10023
9964
  {message.importRunId ? <div className="news-desk-detail-line"><span>Import run</span><strong>{message.importRunId}</strong></div> : null}
10024
9965
  </div>
10025
9966
  <PayloadDetailBlock
@@ -10038,81 +9979,6 @@ function MessageDetailPanel({
10038
9979
  );
10039
9980
  }
10040
9981
 
10041
- function useReferenceCurationMessageReference(
10042
- graph: ReturnType<typeof createSemanticGraphSnapshot>,
10043
- message: MessageRecord,
10044
- metadata: Record<string, unknown> | null,
10045
- ): ReferenceRecord | null {
10046
- const relationReferenceId = useMemo(
10047
- () => referenceCurationMessageReferenceId(graph, message),
10048
- [graph, message],
10049
- );
10050
- const graphReference = useMemo(
10051
- () => resolveReferenceCurationMessageReference(graph, message, metadata),
10052
- [graph, message, metadata],
10053
- );
10054
- const referenceId = relationReferenceId ?? normalizeMetadataString(metadata?.referenceId);
10055
- const [loadedReference, setLoadedReference] = useState<ReferenceRecord | null>(null);
10056
-
10057
- useEffect(() => {
10058
- setLoadedReference(null);
10059
- if (message.messageKind !== "reference_curation" || graphReference || !referenceId) return;
10060
- let active = true;
10061
- loadReferenceRecordById(referenceId)
10062
- .then((reference) => {
10063
- if (active) setLoadedReference(reference);
10064
- })
10065
- .catch(() => {
10066
- if (active) setLoadedReference(null);
10067
- });
10068
- return () => {
10069
- active = false;
10070
- };
10071
- }, [graphReference, message.id, message.messageKind, referenceId]);
10072
-
10073
- return graphReference ?? loadedReference;
10074
- }
10075
-
10076
- function resolveReferenceCurationMessageReference(
10077
- graph: ReturnType<typeof createSemanticGraphSnapshot>,
10078
- message: MessageRecord,
10079
- metadata: Record<string, unknown> | null,
10080
- ): ReferenceRecord | null {
10081
- if (message.messageKind !== "reference_curation") return null;
10082
- const outgoingReference = referenceCurationMessageRelation(graph, message);
10083
- const graphSummary = outgoingReference
10084
- ? graph.resolve("reference", outgoingReference.objectLineageId) ?? graph.resolve("reference", outgoingReference.objectId)
10085
- : null;
10086
- if (graphSummary?.kind === "reference" && graphSummary.record) {
10087
- return graphSummary.record as ReferenceRecord;
10088
- }
10089
- const referenceId = normalizeMetadataString(metadata?.referenceId);
10090
- const referenceLineageId = normalizeMetadataString(metadata?.referenceLineageId);
10091
- const fallbackSummary = (referenceId ? graph.resolve("reference", referenceId) : null)
10092
- ?? (referenceLineageId ? graph.resolve("reference", referenceLineageId) : null);
10093
- return fallbackSummary?.kind === "reference" && fallbackSummary.record
10094
- ? fallbackSummary.record as ReferenceRecord
10095
- : null;
10096
- }
10097
-
10098
- function referenceCurationMessageReferenceId(
10099
- graph: ReturnType<typeof createSemanticGraphSnapshot>,
10100
- message: MessageRecord,
10101
- ): string | null {
10102
- const relation = referenceCurationMessageRelation(graph, message);
10103
- return relation?.objectId ?? null;
10104
- }
10105
-
10106
- function referenceCurationMessageRelation(
10107
- graph: ReturnType<typeof createSemanticGraphSnapshot>,
10108
- message: MessageRecord,
10109
- ): SemanticRelationRecord | null {
10110
- if (message.messageKind !== "reference_curation") return null;
10111
- return graph.outgoing("message", message.id)
10112
- .find((relation) => relation.objectKind === "reference" && relationTypeKey(relation) === "comment")
10113
- ?? null;
10114
- }
10115
-
10116
9982
  type ReferenceCurationDisplayState = {
10117
9983
  effectiveDisplayedStars: number;
10118
9984
  effectiveStatus: string;
@@ -15128,6 +14994,8 @@ function buildReviewedReferenceRecord(
15128
14994
  nextStatus: string;
15129
14995
  note: string | null;
15130
14996
  now: string;
14997
+ /** The details of a person's vote, as the curation mutation records them on the Reference. */
14998
+ vote?: { reasonCode: string | null; sectionId: string | null; decisionRelationId: string | null; shareable: boolean | null };
15131
14999
  },
15132
15000
  ): ReferenceRecord {
15133
15001
  const current = records.find((entry) => entry.id === reference.id) ?? reference;
@@ -15138,6 +15006,14 @@ function buildReviewedReferenceRecord(
15138
15006
  curationStatusUpdatedAt: options.now,
15139
15007
  curationStatusUpdatedBy: options.actorLabel,
15140
15008
  curationStatusReason: options.note,
15009
+ ...(options.vote ? {
15010
+ curationActorKind: "human",
15011
+ curationReasonCode: options.vote.reasonCode,
15012
+ curationSectionId: options.vote.sectionId,
15013
+ curationShareable: options.vote.decisionRelationId ? options.vote.shareable === true : null,
15014
+ curationDelegation: null,
15015
+ curationDecisionRelationId: options.vote.decisionRelationId,
15016
+ } : {}),
15141
15017
  updatedAt: options.now,
15142
15018
  };
15143
15019
  }
@@ -280,6 +280,13 @@ export type ReferenceRecord = {
280
280
  curationStatusUpdatedAt?: string | null;
281
281
  curationStatusUpdatedBy?: string | null;
282
282
  curationStatusReason?: string | null;
283
+ /** The latest vote's details; a Reference keeps only its latest vote (earlier votes live in the Cyclotron's feedback). */
284
+ curationReasonCode?: string | null;
285
+ curationActorKind?: string | null;
286
+ curationSectionId?: string | null;
287
+ curationShareable?: boolean | null;
288
+ curationDelegation?: unknown;
289
+ curationDecisionRelationId?: string | null;
283
290
  newsroomFeedKey?: string | null;
284
291
  reviewedFeedKey?: string | null;
285
292
  metadata?: unknown;
@@ -1,4 +1,4 @@
1
- import type { MessageRecord } from "./category-repository";
1
+ import type { ReferenceRecord } from "./category-repository";
2
2
  import type { RelevanceCuration, RelevanceDecisionView } from "./relevance-decisions";
3
3
  import { safeId } from "./relevance-status";
4
4
 
@@ -149,22 +149,12 @@ function parseMetadata(value: unknown): Record<string, unknown> {
149
149
  }
150
150
 
151
151
  /**
152
- * Reference lineage ids whose latest curation Message was made by a signed-in
153
- * person. Agent and classifier status changes never appear here, and neither
154
- * does a reference a person decided before a later non-human change.
152
+ * Reference lineage ids whose latest vote was made by a signed-in person
153
+ * (`curationActorKind` is `human`). Agent, Cyclotron and rule votes never appear
154
+ * here, and neither does a reference a person decided before a later non-human change.
155
155
  */
156
- export function referencesWithHumanLatestCuration(messages: MessageRecord[]): Set<string> {
157
- const latestByReference = new Map<string, { createdAt: string; human: boolean }>();
158
- for (const message of messages) {
159
- if (message.messageKind !== "reference_curation") continue;
160
- const metadata = parseMetadata(message.metadata);
161
- const lineageId = typeof metadata.referenceLineageId === "string" ? metadata.referenceLineageId : "";
162
- if (!lineageId) continue;
163
- const seen = latestByReference.get(lineageId);
164
- if (seen && seen.createdAt >= message.createdAt) continue;
165
- latestByReference.set(lineageId, { createdAt: message.createdAt, human: metadata.actorKind === "human" });
166
- }
167
- return new Set([...latestByReference].filter(([, entry]) => entry.human).map(([lineageId]) => lineageId));
156
+ export function referencesWithHumanLatestCuration(references: readonly ReferenceRecord[]): Set<string> {
157
+ return new Set(references.filter((reference) => reference.curationActorKind === "human").map((reference) => reference.lineageId ?? reference.id));
168
158
  }
169
159
 
170
160
  export type SuggestionChoice = "agree" | "overrule";
@@ -233,30 +223,22 @@ export function delegationShortId(delegationId: string): string {
233
223
  }
234
224
 
235
225
  /**
236
- * Delegated curations by reference lineage, from each reference's latest
237
- * curation Message only. The person is the Message author; the agent is relayedBy.
226
+ * Delegated curations by reference lineage, from each Reference's latest vote only.
227
+ * The person is who set the status (`curationStatusUpdatedBy`); the agent is the delegation's relayedBy.
238
228
  */
239
- export function delegatedCurationsByReference(messages: MessageRecord[]): Map<string, DelegatedCuration> {
240
- const latest = new Map<string, MessageRecord>();
241
- for (const message of messages) {
242
- if (message.messageKind !== "reference_curation") continue;
243
- const lineageId = parseMetadata(message.metadata).referenceLineageId;
244
- if (typeof lineageId !== "string" || !lineageId) continue;
245
- const seen = latest.get(lineageId);
246
- if (!seen || seen.createdAt < message.createdAt) latest.set(lineageId, message);
247
- }
229
+ export function delegatedCurationsByReference(references: readonly ReferenceRecord[]): Map<string, DelegatedCuration> {
248
230
  const delegated = new Map<string, DelegatedCuration>();
249
- for (const [lineageId, message] of latest) {
250
- const metadata = parseMetadata(message.metadata);
251
- if (metadata.attestation !== "delegated") continue;
252
- const delegationId = typeof metadata.delegationId === "string" ? metadata.delegationId : "";
253
- delegated.set(lineageId, {
254
- agent: typeof metadata.relayedBy === "string" && metadata.relayedBy ? metadata.relayedBy : "an agent",
255
- person: message.authorLabel?.trim() || null,
256
- instruction: typeof metadata.instruction === "string" ? metadata.instruction : "",
231
+ for (const reference of references) {
232
+ const delegation = parseMetadata(reference.curationDelegation);
233
+ if (!Object.keys(delegation).length) continue;
234
+ const delegationId = typeof delegation.delegationId === "string" ? delegation.delegationId : "";
235
+ delegated.set(reference.lineageId ?? reference.id, {
236
+ agent: typeof delegation.relayedBy === "string" && delegation.relayedBy ? delegation.relayedBy : "an agent",
237
+ person: reference.curationStatusUpdatedBy?.trim() || null,
238
+ instruction: typeof delegation.instruction === "string" ? delegation.instruction : "",
257
239
  delegationId,
258
240
  delegationShortId: delegationShortId(delegationId),
259
- countsAsTrainingLabel: metadata.actorKind === "human",
241
+ countsAsTrainingLabel: reference.curationActorKind === "human",
260
242
  });
261
243
  }
262
244
  return delegated;
@@ -210,15 +210,16 @@ const PREVIEW_CYCLOTRON_ID = "papyrus-relevance";
210
210
  /**
211
211
  * Fixture data for reviewing the cyclotron suggestion UI without a backend:
212
212
  * suggestions with provenance on the first references, a mix of human and
213
- * non-human curation messages, and a live-agreement payload.
213
+ * non-human votes recorded on the References, and a live-agreement payload.
214
214
  */
215
215
  export function createLocalPreviewCyclotronFixtures(references: ReferenceRecord[]): {
216
216
  relations: SemanticRelationRecord[];
217
- messages: MessageRecord[];
217
+ /** The first few References with a latest vote recorded on them, by Reference id. */
218
+ votedReferences: Map<string, ReferenceRecord>;
218
219
  shadowAgreement: ShadowAgreement;
219
220
  } {
220
221
  const relations: SemanticRelationRecord[] = [];
221
- const messages: MessageRecord[] = [];
222
+ const votedReferences = new Map<string, ReferenceRecord>();
222
223
  references.slice(0, PREVIEW_SUGGESTION_PLANS.length).forEach((reference, index) => {
223
224
  const plan = PREVIEW_SUGGESTION_PLANS[index];
224
225
  const lineageId = reference.lineageId ?? reference.id;
@@ -264,35 +265,23 @@ export function createLocalPreviewCyclotronFixtures(references: ReferenceRecord[
264
265
  }),
265
266
  } as SemanticRelationRecord);
266
267
  if (plan.curation !== "none") {
267
- const createdAt = isoTimestampHoursBeforeBase(30 - index);
268
- messages.push({
269
- id: `preview-curation-message-${index}`,
270
- messageKind: "reference_curation",
271
- messageDomain: "commentary",
272
- status: "active",
273
- summary: `${reference.title ?? lineageId}: curated`,
274
- authorLabel: plan.curation === "delegated" ? "Maya Okafor" : null,
275
- source: "newsroom",
276
- createdAt,
277
- updatedAt: createdAt,
278
- metadata: JSON.stringify({
279
- action: reference.curationStatus === "rejected" ? "reject" : "accept",
280
- curationStatus: reference.curationStatus,
281
- referenceLineageId: lineageId,
282
- actorKind: plan.curation === "service" ? "service" : "human",
283
- ...(plan.curation === "delegated" ? {
284
- attestation: "delegated",
285
- relayedBy: "newsroom-reporter",
286
- delegationId: "curation-delegation-9b1f6c24-5d0e-4a7b-8c31-2e6f0a1d7b45",
287
- instruction: "Reject that one, it duplicates the filing from Tuesday.",
288
- } : {}),
289
- }),
268
+ // A Reference keeps its latest vote on itself (no Message): the details sit in its curation* fields.
269
+ votedReferences.set(reference.id, {
270
+ ...reference,
271
+ curationStatusUpdatedAt: isoTimestampHoursBeforeBase(30 - index),
272
+ curationStatusUpdatedBy: plan.curation === "delegated" ? "Maya Okafor" : reference.curationStatusUpdatedBy,
273
+ curationActorKind: plan.curation === "service" ? "agent" : "human",
274
+ curationDelegation: plan.curation === "delegated" ? {
275
+ delegationId: "curation-delegation-9b1f6c24",
276
+ relayedBy: "newsroom-reporter",
277
+ instruction: "Reject that one, it duplicates the filing from Tuesday.",
278
+ } : null,
290
279
  });
291
280
  }
292
281
  });
293
282
  return {
294
283
  relations,
295
- messages,
284
+ votedReferences,
296
285
  shadowAgreement: {
297
286
  schema: SHADOW_AGREEMENT_SCHEMA,
298
287
  comparisons: 23,
@@ -139,23 +139,17 @@ export function isEvidenceEligibleReference(reference: Pick<ReferenceRecord, "ve
139
139
  && normalizeReferenceCurationStatus(reference?.curationStatus) === "accepted";
140
140
  }
141
141
 
142
- export function referenceReasonCode(reference: Pick<ReferenceRecord, "metadata"> | null | undefined, messages: Array<Pick<MessageRecord, "metadata" | "createdAt">> = []): ReferenceRejectionReasonCode | null {
143
- const metadataReason = reasonCodeFromMetadata(reference?.metadata);
144
- if (metadataReason) return metadataReason;
145
- const sortedMessages = [...messages].sort((left, right) => String(right.createdAt ?? "").localeCompare(String(left.createdAt ?? "")));
146
- for (const message of sortedMessages) {
147
- const reason = reasonCodeFromMetadata(message.metadata);
148
- if (reason) return reason;
149
- }
150
- return null;
142
+ /** The reason code of the Reference's latest vote (`curationReasonCode`), else one recorded in its metadata. */
143
+ export function referenceReasonCode(reference: Pick<ReferenceRecord, "metadata" | "curationReasonCode"> | null | undefined): ReferenceRejectionReasonCode | null {
144
+ return normalizeReferenceRejectionReasonCode(reference?.curationReasonCode) ?? reasonCodeFromMetadata(reference?.metadata);
151
145
  }
152
146
 
153
- export function scopeTrainingLabelForReference(reference: Pick<ReferenceRecord, "versionState" | "curationStatus" | "metadata"> | null | undefined, messages: Array<Pick<MessageRecord, "metadata" | "createdAt">> = []): "positive" | "negative" | null {
147
+ export function scopeTrainingLabelForReference(reference: Pick<ReferenceRecord, "versionState" | "curationStatus" | "metadata" | "curationReasonCode"> | null | undefined): "positive" | "negative" | null {
154
148
  if (!reference || reference.versionState !== "current") return null;
155
149
  const status = normalizeReferenceCurationStatus(reference.curationStatus);
156
150
  if (status === "accepted") return "positive";
157
151
  if (status !== "rejected") return null;
158
- const reasonCode = referenceReasonCode(reference, messages);
152
+ const reasonCode = referenceReasonCode(reference);
159
153
  return reasonCode && SCOPE_TRAINING_NEGATIVE_REASON_CODE_SET.has(reasonCode) ? "negative" : null;
160
154
  }
161
155
 
@@ -1,71 +1,35 @@
1
- import type { HydratedModelPayload, MessageRecord, SemanticRelationRecord } from "./category-repository";
1
+ import type { ReferenceRecord, SemanticRelationRecord } from "./category-repository";
2
2
  import { normalizeReferenceRejectionReasonCode, type ReferenceRejectionReasonCode } from "./reference-policy";
3
3
 
4
- function metadataRecord(value: unknown): Record<string, unknown> {
5
- if (typeof value === "string") {
6
- try { return metadataRecord(JSON.parse(value)); } catch { return {}; }
7
- }
8
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
9
- }
10
-
11
- /** The selected Reference's index is authoritative even when the global feed is truncated. */
4
+ /**
5
+ * The selected Reference's indexed predictions (the Cyclotron's relevance decisions). The Reference's own latest vote
6
+ * (`curationActorKind`, `curationReasonCode`, ...) is already on the Reference, so no Message is read.
7
+ */
12
8
  export async function loadReferenceReviewDetails(lineageId: string, read: {
13
- relations: (direction: "incoming" | "outgoing") => Promise<SemanticRelationRecord[]>;
14
- message: (id: string) => Promise<MessageRecord | null>;
15
- payloads: (id: string) => Promise<HydratedModelPayload[]>;
16
- }): Promise<{ relations: SemanticRelationRecord[]; messages: MessageRecord[] }> {
17
- const [outgoing, incoming] = await Promise.all([read.relations("outgoing"), read.relations("incoming")]);
18
- const relations = outgoing.filter((relation) => relation.subjectKind === "reference" && relation.subjectLineageId === lineageId);
19
- const latest = incoming.filter((relation) => relation.predicate === "comment" && relation.relationState === "current"
20
- && relation.subjectKind === "message" && relation.objectKind === "reference" && relation.objectLineageId === lineageId
21
- && metadataRecord(relation.metadata).messageKind === "reference_curation")
22
- .sort((a, b) => String(b.createdAt ?? b.importedAt ?? "").localeCompare(String(a.createdAt ?? a.importedAt ?? "")))[0];
23
- if (!latest) return { relations, messages: [] };
24
- const message = await read.message(latest.subjectId);
25
- if (!message || message.messageKind !== "reference_curation") throw new Error("The latest reference review could not be loaded.");
26
- const payload = (await read.payloads(message.id))
27
- .filter((entry) => entry.attachment.role === "metadata" && entry.attachment.ownerKind === "message" && entry.attachment.ownerId === message.id)
28
- .sort((a, b) => String(b.attachment.updatedAt ?? "").localeCompare(String(a.attachment.updatedAt ?? "")))[0];
29
- if (!payload) throw new Error("The latest reference review metadata could not be loaded.");
30
- if (payload.error) throw new Error(payload.error);
31
- if (!payload.json || typeof payload.json !== "object" || Array.isArray(payload.json)) {
32
- throw new Error("The latest reference review metadata is invalid.");
33
- }
34
- const hydrated = { ...message, metadata: payload.json };
35
- const recordedLineage = metadataRecord(hydrated.metadata).referenceLineageId;
36
- if (recordedLineage !== lineageId) throw new Error("The latest review metadata does not match this reference lineage.");
37
- return { relations, messages: [hydrated] };
38
- }
39
-
40
- function latestReferenceReviews(messages: MessageRecord[]): Map<string, MessageRecord> {
41
- const latest = new Map<string, MessageRecord>();
42
- for (const message of messages) {
43
- if (message.messageKind !== "reference_curation") continue;
44
- const lineage = metadataRecord(message.metadata).referenceLineageId;
45
- if (typeof lineage !== "string" || !lineage) continue;
46
- if (!latest.has(lineage) || String(message.createdAt).localeCompare(String(latest.get(lineage)!.createdAt)) > 0) latest.set(lineage, message);
47
- }
48
- return latest;
9
+ relations: (direction: "outgoing") => Promise<SemanticRelationRecord[]>;
10
+ }): Promise<{ relations: SemanticRelationRecord[] }> {
11
+ const outgoing = await read.relations("outgoing");
12
+ return { relations: outgoing.filter((relation) => relation.subjectKind === "reference" && relation.subjectLineageId === lineageId) };
49
13
  }
50
14
 
51
- /** A later acceptance or archive clears any earlier rejection reason. */
52
- export function latestReferenceReviewReasons(messages: MessageRecord[]): Map<string, ReferenceRejectionReasonCode> {
15
+ /** The reason code of each rejected Reference's latest vote. A later acceptance or archive replaced the vote, so it clears the reason. */
16
+ export function latestReferenceReviewReasons(references: readonly ReferenceRecord[]): Map<string, ReferenceRejectionReasonCode> {
53
17
  const reasons = new Map<string, ReferenceRejectionReasonCode>();
54
- for (const [lineage, message] of latestReferenceReviews(messages)) {
55
- const metadata = metadataRecord(message.metadata);
56
- const reason = metadata.action === "reject" ? normalizeReferenceRejectionReasonCode(metadata.reasonCode) : null;
57
- if (reason) reasons.set(lineage, reason);
18
+ for (const reference of references) {
19
+ if (!reference.curationActorKind || reference.curationStatus !== "rejected") continue;
20
+ const reason = normalizeReferenceRejectionReasonCode(reference.curationReasonCode);
21
+ if (reason) reasons.set(reference.lineageId ?? reference.id, reason);
58
22
  }
59
23
  return reasons;
60
24
  }
61
25
 
62
- /** Preserve only a Section explicitly recorded on the newest acceptance, never a prediction. */
63
- export function latestReferenceReviewSections(messages: MessageRecord[]): Map<string, string> {
26
+ /** The Section a person explicitly recorded on a Reference's latest vote when accepting it, never a prediction. */
27
+ export function latestReferenceReviewSections(references: readonly ReferenceRecord[]): Map<string, string> {
64
28
  const sections = new Map<string, string>();
65
- for (const [lineage, message] of latestReferenceReviews(messages)) {
66
- const metadata = metadataRecord(message.metadata);
67
- const sectionId = metadata.action === "accept" && typeof metadata.sectionId === "string" ? metadata.sectionId.trim() : "";
68
- if (sectionId) sections.set(lineage, sectionId);
29
+ for (const reference of references) {
30
+ if (!reference.curationActorKind || reference.curationStatus !== "accepted") continue;
31
+ const sectionId = reference.curationSectionId?.trim() ?? "";
32
+ if (sectionId) sections.set(reference.lineageId ?? reference.id, sectionId);
69
33
  }
70
34
  return sections;
71
35
  }
package/package.json CHANGED
@@ -517,7 +517,7 @@
517
517
  "optional": true
518
518
  }
519
519
  },
520
- "version": "5.43.0-next.47",
520
+ "version": "5.43.0-next.48",
521
521
  "dependencies": {
522
522
  "@aws-amplify/adapter-nextjs": "^1.7.3",
523
523
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.43.0-next.47",
2
+ "version": "5.43.0-next.48",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",