@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.
- package/amplify/data/resource.js +6 -2
- package/amplify/data/resource.ts +6 -2
- package/amplify/data/schema.js +6 -2
- package/amplify/data/schema.ts +6 -2
- package/amplify/functions/category-action/handler.ts +18 -118
- package/amplify/functions/shared/reference-curation-metadata.ts +21 -29
- package/components/news-desk-taxonomy-client.ts +10 -12
- package/components/topic-steering-workspace.tsx +38 -162
- package/lib/category-repository.ts +7 -0
- package/lib/cyclotron-suggestions.ts +18 -36
- package/lib/local-preview-seed.ts +16 -27
- package/lib/reference-policy.ts +5 -11
- package/lib/reference-review-details.ts +21 -57
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
package/amplify/data/resource.js
CHANGED
|
@@ -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
|
package/amplify/data/resource.ts
CHANGED
|
@@ -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
|
package/amplify/data/schema.js
CHANGED
|
@@ -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
|
package/amplify/data/schema.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
*
|
|
3
|
-
* relevance cyclotron decided the reference, the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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" | "
|
|
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" : "
|
|
26
|
+
return typeof subject === "string" && subject.trim() !== "" ? "human" : "agent"
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 (
|
|
2156
|
+
relations: async () => {
|
|
2157
2157
|
if (testMock?.semanticRelations) return testMock.semanticRelations;
|
|
2158
2158
|
return loadSemanticRelationsByState({
|
|
2159
|
-
field:
|
|
2160
|
-
keyName:
|
|
2159
|
+
field: "listSemanticRelationsBySubjectState",
|
|
2160
|
+
keyName: "subjectStateKey",
|
|
2161
2161
|
keyValue: `reference#${lineageId}#current`,
|
|
2162
|
-
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(
|
|
785
|
-
[
|
|
789
|
+
() => delegatedCurationsByReference(votedReferences),
|
|
790
|
+
[votedReferences],
|
|
786
791
|
);
|
|
787
792
|
const humanCuratedReferenceLineageIds = useMemo(
|
|
788
|
-
() => referencesWithHumanLatestCuration(
|
|
789
|
-
[
|
|
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(
|
|
909
|
-
const reviewSectionIds = useMemo(() => latestReferenceReviewSections(
|
|
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
|
|
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
|
-
|
|
1649
|
-
|
|
1635
|
+
sectionId: action === "accept" ? sectionId ?? null : null,
|
|
1636
|
+
decisionRelationId: relevance?.decisionRelationId ?? null,
|
|
1637
|
+
shareable: relevance ? relevance.shareable : null,
|
|
1650
1638
|
},
|
|
1651
|
-
}
|
|
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.
|
|
9981
|
-
|
|
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>
|
|
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 {
|
|
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
|
|
153
|
-
*
|
|
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(
|
|
157
|
-
|
|
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
|
|
237
|
-
*
|
|
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(
|
|
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
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
const delegationId = typeof
|
|
253
|
-
delegated.set(lineageId, {
|
|
254
|
-
agent: typeof
|
|
255
|
-
person:
|
|
256
|
-
instruction: typeof
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
284
|
+
votedReferences,
|
|
296
285
|
shadowAgreement: {
|
|
297
286
|
schema: SHADOW_AGREEMENT_SCHEMA,
|
|
298
287
|
comparisons: 23,
|
package/lib/reference-policy.ts
CHANGED
|
@@ -139,23 +139,17 @@ export function isEvidenceEligibleReference(reference: Pick<ReferenceRecord, "ve
|
|
|
139
139
|
&& normalizeReferenceCurationStatus(reference?.curationStatus) === "accepted";
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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"
|
|
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
|
|
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 {
|
|
1
|
+
import type { ReferenceRecord, SemanticRelationRecord } from "./category-repository";
|
|
2
2
|
import { normalizeReferenceRejectionReasonCode, type ReferenceRejectionReasonCode } from "./reference-policy";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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: "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
52
|
-
export function latestReferenceReviewReasons(
|
|
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
|
|
55
|
-
|
|
56
|
-
const reason =
|
|
57
|
-
if (reason) reasons.set(
|
|
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
|
-
/**
|
|
63
|
-
export function latestReferenceReviewSections(
|
|
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
|
|
66
|
-
|
|
67
|
-
const sectionId =
|
|
68
|
-
if (sectionId) sections.set(
|
|
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
package/routes.manifest.json
CHANGED