@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +47 -0
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getAmplifyDataClientConfig } from "@aws-amplify/backend/function/runtime";
|
|
7
|
+
import { Amplify } from "aws-amplify";
|
|
8
|
+
import { generateClient } from "aws-amplify/data";
|
|
9
|
+
import type { Schema } from "../../data/resource";
|
|
10
|
+
import { putJsonModelPayload, readJsonModelPayload } from "../shared/model-payloads";
|
|
11
|
+
|
|
12
|
+
type AssignmentHandler =
|
|
13
|
+
| Schema["claimAssignment"]["functionHandler"]
|
|
14
|
+
| Schema["releaseAssignment"]["functionHandler"]
|
|
15
|
+
| Schema["completeAssignment"]["functionHandler"]
|
|
16
|
+
| Schema["cancelAssignment"]["functionHandler"]
|
|
17
|
+
| Schema["reopenAssignment"]["functionHandler"]
|
|
18
|
+
| Schema["retryImmediateAssignment"]["functionHandler"]
|
|
19
|
+
| Schema["getAssignmentContext"]["functionHandler"]
|
|
20
|
+
| Schema["listAssignmentQueue"]["functionHandler"]
|
|
21
|
+
| Schema["listAssignmentsForObject"]["functionHandler"];
|
|
22
|
+
type DataClient = ReturnType<typeof generateClient<Schema>>;
|
|
23
|
+
type DataClientErrors = Array<{ message?: string | null } | string | null> | null | undefined;
|
|
24
|
+
type DataClientResult<T = unknown> = {
|
|
25
|
+
data?: T | null;
|
|
26
|
+
errors?: DataClientErrors;
|
|
27
|
+
nextToken?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FINAL_STATUSES = new Set(["completed", "canceled"]);
|
|
31
|
+
const IMMEDIATE_ASSIGNMENT_TYPES = new Set(["procedure.run"]);
|
|
32
|
+
const NEWSROOM_SUMMARY_PAYLOAD_ID = "knowledge-raw-payload-newsroom-summary-current";
|
|
33
|
+
const NEWSROOM_SUMMARY_PAYLOAD_OWNER_KIND = "knowledgeRawPayload";
|
|
34
|
+
const SUMMARY_STALE_AFTER_MS = 15 * 60 * 1000;
|
|
35
|
+
const PUBLICATION_DOCTRINE_DEFINITIONS = [
|
|
36
|
+
{ scope: "publication", kind: "mission", label: "Editorial Mission", slug: "editorial-doctrine-mission" },
|
|
37
|
+
{ scope: "publication", kind: "policy", label: "Editorial Policy", slug: "editorial-doctrine-policy" },
|
|
38
|
+
] as const;
|
|
39
|
+
let clientPromise: Promise<DataClient> | null = null;
|
|
40
|
+
|
|
41
|
+
export const handler = async (event: any): Promise<any> => {
|
|
42
|
+
const fieldName = normalizeRequiredString(event.info?.fieldName ?? event.fieldName, "fieldName");
|
|
43
|
+
if (fieldName === "getAssignmentContext") return getAssignmentContext(event as Parameters<Schema["getAssignmentContext"]["functionHandler"]>[0]);
|
|
44
|
+
if (fieldName === "listAssignmentQueue") return listAssignmentQueue(event as Parameters<Schema["listAssignmentQueue"]["functionHandler"]>[0]);
|
|
45
|
+
if (fieldName === "listAssignmentsForObject") return listAssignmentsForObject(event as Parameters<Schema["listAssignmentsForObject"]["functionHandler"]>[0]);
|
|
46
|
+
if (fieldName === "retryImmediateAssignment") return retryImmediateAssignment(event as Parameters<Schema["retryImmediateAssignment"]["functionHandler"]>[0]);
|
|
47
|
+
return mutateAssignment(event as Parameters<Schema["claimAssignment"]["functionHandler"]>[0], fieldName);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
async function mutateAssignment(event: Parameters<Schema["claimAssignment"]["functionHandler"]>[0], fieldName: string) {
|
|
51
|
+
const client = await getDataClient();
|
|
52
|
+
const assignmentId = normalizeRequiredString(event.arguments.assignmentId, "assignmentId");
|
|
53
|
+
const assignment = await getRequiredRecord(client.models.Assignment, assignmentId, "Assignment");
|
|
54
|
+
const action = assignmentActionFromField(fieldName);
|
|
55
|
+
const now = new Date().toISOString();
|
|
56
|
+
const actorSub = normalizeOptionalString(event.arguments.actorSub) ?? getIdentitySub(event);
|
|
57
|
+
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
58
|
+
const next = nextAssignmentUpdate(assignment, action, event.arguments, now);
|
|
59
|
+
await requireDataResult(client.models.Assignment.update({ id: assignmentId, ...next }), `update Assignment ${assignmentId}`);
|
|
60
|
+
const eventId = `assignment-event-${assignmentId}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
61
|
+
const eventMetadata: Record<string, unknown> = { fieldName };
|
|
62
|
+
if (action === "claim") {
|
|
63
|
+
eventMetadata.assigneeKey = next.assigneeKey ?? null;
|
|
64
|
+
eventMetadata.claimExpiresAt = next.claimExpiresAt ?? null;
|
|
65
|
+
eventMetadata.previousAssigneeKey = normalizeOptionalString(assignment.assigneeKey);
|
|
66
|
+
}
|
|
67
|
+
await requireDataResult(
|
|
68
|
+
client.models.AssignmentEvent.create({
|
|
69
|
+
id: eventId,
|
|
70
|
+
assignmentId,
|
|
71
|
+
assignmentTypeKey: assignment.assignmentTypeKey,
|
|
72
|
+
queueKey: assignment.queueKey,
|
|
73
|
+
eventType: action,
|
|
74
|
+
fromStatus: assignment.status,
|
|
75
|
+
toStatus: next.status ?? assignment.status,
|
|
76
|
+
actorSub,
|
|
77
|
+
actorLabel,
|
|
78
|
+
note: normalizeOptionalString(event.arguments.note),
|
|
79
|
+
createdAt: now,
|
|
80
|
+
}),
|
|
81
|
+
`create AssignmentEvent ${eventId}`,
|
|
82
|
+
);
|
|
83
|
+
await putJsonModelPayload(
|
|
84
|
+
client as any,
|
|
85
|
+
{ ownerKind: "assignmentEvent", ownerId: eventId, ownerLineageId: eventId },
|
|
86
|
+
"metadata",
|
|
87
|
+
"metadata",
|
|
88
|
+
eventMetadata,
|
|
89
|
+
{ filename: "metadata.json", now },
|
|
90
|
+
);
|
|
91
|
+
await updateNewsroomSummaryForAssignmentAction(client, {
|
|
92
|
+
assignmentTypeKey: normalizeOptionalString(assignment.assignmentTypeKey),
|
|
93
|
+
sectionKey: assignmentSectionKey(assignment),
|
|
94
|
+
previousStatus: normalizeOptionalString(assignment.status),
|
|
95
|
+
nextStatus: normalizeOptionalString(next.status) ?? normalizeOptionalString(assignment.status),
|
|
96
|
+
now,
|
|
97
|
+
});
|
|
98
|
+
let finalStatus = normalizeOptionalString(next.status) ?? normalizeOptionalString(assignment.status) ?? "open";
|
|
99
|
+
if (action === "claim" && assignmentExecutionMode(assignment.assignmentTypeKey) === "immediate") {
|
|
100
|
+
const execution = await executeImmediateClaimedAssignment(client, {
|
|
101
|
+
assignment: { ...assignment, ...next, id: assignmentId },
|
|
102
|
+
actorSub,
|
|
103
|
+
actorLabel,
|
|
104
|
+
now: new Date().toISOString(),
|
|
105
|
+
note: normalizeOptionalString(event.arguments.note),
|
|
106
|
+
trigger: "claim",
|
|
107
|
+
});
|
|
108
|
+
finalStatus = execution.status;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
assignmentId,
|
|
113
|
+
eventId,
|
|
114
|
+
status: finalStatus,
|
|
115
|
+
action,
|
|
116
|
+
assigneeKey: normalizeOptionalString(next.assigneeKey),
|
|
117
|
+
claimExpiresAt: normalizeOptionalString(next.claimExpiresAt),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function retryImmediateAssignment(event: Parameters<Schema["retryImmediateAssignment"]["functionHandler"]>[0]) {
|
|
122
|
+
const client = await getDataClient();
|
|
123
|
+
const assignmentId = normalizeRequiredString(event.arguments.assignmentId, "assignmentId");
|
|
124
|
+
const assignment = await getRequiredRecord(client.models.Assignment, assignmentId, "Assignment");
|
|
125
|
+
if (assignmentExecutionMode(assignment.assignmentTypeKey) !== "immediate") {
|
|
126
|
+
throw new Error(`Assignment ${assignmentId} is ${assignment.assignmentTypeKey}; only immediate assignment types support retry.`);
|
|
127
|
+
}
|
|
128
|
+
if (normalizeOptionalString(assignment.status) !== "open") {
|
|
129
|
+
throw new Error(`Assignment ${assignmentId} is ${assignment.status}; retry requires open status.`);
|
|
130
|
+
}
|
|
131
|
+
const actorSub = normalizeOptionalString(event.arguments.actorSub) ?? getIdentitySub(event);
|
|
132
|
+
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
133
|
+
const now = new Date().toISOString();
|
|
134
|
+
const retryRunId = normalizeOptionalString(assignment.assignmentTypeKey) === "procedure.run"
|
|
135
|
+
? await cloneProcedureRunForRetry(client, assignment, now, actorLabel ?? actorSub ?? "Papyrus newsroom")
|
|
136
|
+
: null;
|
|
137
|
+
const claimIdentity = resolveClaimIdentity({
|
|
138
|
+
assigneeType: "user",
|
|
139
|
+
assigneeId: actorSub ?? actorLabel ?? "unknown",
|
|
140
|
+
assigneeKey: `user#${actorSub ?? actorLabel ?? "unknown"}`,
|
|
141
|
+
});
|
|
142
|
+
const claimedUpdate = {
|
|
143
|
+
status: "claimed",
|
|
144
|
+
queueStatusKey: `${assignment.queueKey}#claimed`,
|
|
145
|
+
sectionStatusKey: sectionStatusKey(assignment, "claimed"),
|
|
146
|
+
sectionQueueStatusKey: sectionQueueStatusKey(assignment, "claimed"),
|
|
147
|
+
assigneeType: claimIdentity.assigneeType,
|
|
148
|
+
assigneeId: claimIdentity.assigneeId,
|
|
149
|
+
assigneeKey: claimIdentity.assigneeKey,
|
|
150
|
+
claimedAt: now,
|
|
151
|
+
claimExpiresAt: null,
|
|
152
|
+
sourceSnapshotId: retryRunId ?? assignment.sourceSnapshotId ?? null,
|
|
153
|
+
updatedAt: now,
|
|
154
|
+
};
|
|
155
|
+
await requireDataResult(client.models.Assignment.update({ id: assignmentId, ...claimedUpdate }), `retry claim Assignment ${assignmentId}`);
|
|
156
|
+
const retryEventId = `assignment-event-${assignmentId}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
157
|
+
await requireDataResult(
|
|
158
|
+
client.models.AssignmentEvent.create({
|
|
159
|
+
id: retryEventId,
|
|
160
|
+
assignmentId,
|
|
161
|
+
assignmentTypeKey: assignment.assignmentTypeKey,
|
|
162
|
+
queueKey: assignment.queueKey,
|
|
163
|
+
eventType: "retry",
|
|
164
|
+
fromStatus: assignment.status,
|
|
165
|
+
toStatus: "claimed",
|
|
166
|
+
actorSub,
|
|
167
|
+
actorLabel,
|
|
168
|
+
note: normalizeOptionalString(event.arguments.note) ?? "manual immediate retry",
|
|
169
|
+
createdAt: now,
|
|
170
|
+
}),
|
|
171
|
+
`create AssignmentEvent ${retryEventId}`,
|
|
172
|
+
);
|
|
173
|
+
await updateNewsroomSummaryForAssignmentAction(client, {
|
|
174
|
+
assignmentTypeKey: normalizeOptionalString(assignment.assignmentTypeKey),
|
|
175
|
+
sectionKey: assignmentSectionKey(assignment),
|
|
176
|
+
previousStatus: normalizeOptionalString(assignment.status),
|
|
177
|
+
nextStatus: "claimed",
|
|
178
|
+
now,
|
|
179
|
+
});
|
|
180
|
+
const execution = await executeImmediateClaimedAssignment(client, {
|
|
181
|
+
assignment: { ...assignment, ...claimedUpdate, id: assignmentId },
|
|
182
|
+
actorSub,
|
|
183
|
+
actorLabel,
|
|
184
|
+
now: new Date().toISOString(),
|
|
185
|
+
note: normalizeOptionalString(event.arguments.note),
|
|
186
|
+
trigger: "retry",
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
assignmentId,
|
|
191
|
+
eventId: retryEventId,
|
|
192
|
+
status: execution.status,
|
|
193
|
+
action: "retry",
|
|
194
|
+
assigneeKey: claimIdentity.assigneeKey,
|
|
195
|
+
claimExpiresAt: null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function cloneProcedureRunForRetry(client: DataClient, assignment: any, now: string, actorLabel: string): Promise<string> {
|
|
200
|
+
const sourceRunId = normalizeOptionalString(assignment.sourceSnapshotId);
|
|
201
|
+
if (!sourceRunId) throw new Error(`Assignment ${assignment.id} has no sourceSnapshotId for immediate retry.`);
|
|
202
|
+
const priorRun = await getRequiredRecord((client.models as any).ProcedureRun, sourceRunId, "ProcedureRun");
|
|
203
|
+
const nextAttempt = (normalizeOptionalPositiveInteger(priorRun.attempt) ?? 1) + 1;
|
|
204
|
+
const retryRunId = `procedure-run-retry-${safeId(assignment.id)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 6)}`;
|
|
205
|
+
await requireDataResult((client.models as any).ProcedureRun.create({
|
|
206
|
+
id: retryRunId,
|
|
207
|
+
procedureId: priorRun.procedureId,
|
|
208
|
+
procedureKey: priorRun.procedureKey,
|
|
209
|
+
procedureVersionId: priorRun.procedureVersionId,
|
|
210
|
+
procedureVersionNumber: priorRun.procedureVersionNumber ?? null,
|
|
211
|
+
assignmentId: assignment.id,
|
|
212
|
+
runStatus: "queued",
|
|
213
|
+
requestedBy: actorLabel,
|
|
214
|
+
requestedAt: now,
|
|
215
|
+
startedAt: null,
|
|
216
|
+
finishedAt: null,
|
|
217
|
+
input: toAwsJson(priorRun.input ?? {}),
|
|
218
|
+
normalizedInput: toAwsJson(priorRun.normalizedInput ?? priorRun.input ?? {}),
|
|
219
|
+
resultSummary: null,
|
|
220
|
+
errorSummary: null,
|
|
221
|
+
output: null,
|
|
222
|
+
error: null,
|
|
223
|
+
attempt: nextAttempt,
|
|
224
|
+
newsroomFeedKey: priorRun.newsroomFeedKey ?? "procedures",
|
|
225
|
+
}), `create ProcedureRun retry ${retryRunId}`);
|
|
226
|
+
return retryRunId;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function updateNewsroomSummaryForAssignmentAction(
|
|
230
|
+
client: DataClient,
|
|
231
|
+
input: { assignmentTypeKey: string | null; sectionKey: string | null; previousStatus: string | null; nextStatus: string | null; now: string },
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
const response = await client.models.KnowledgeRawPayload.get({ id: NEWSROOM_SUMMARY_PAYLOAD_ID });
|
|
234
|
+
assertNoDataErrors(response.errors, "get Newsroom summary snapshot");
|
|
235
|
+
const payload = normalizeSummaryPayload(await readNewsroomSummaryPayload(client), input.now);
|
|
236
|
+
payload.generatedAt = input.now;
|
|
237
|
+
payload.staleAt = new Date(Date.parse(input.now) + SUMMARY_STALE_AFTER_MS).toISOString();
|
|
238
|
+
payload.source = "incremental";
|
|
239
|
+
payload.counts.assignmentEvents = Math.max(0, (payload.counts.assignmentEvents ?? 0) + 1);
|
|
240
|
+
payload.counts.modelAttachments = Math.max(0, (payload.counts.modelAttachments ?? 0) + 1);
|
|
241
|
+
increment(payload.facets.modelAttachments.byOwnerKind, "assignmentEvent", 1);
|
|
242
|
+
increment(payload.facets.modelAttachments.byRole, "metadata", 1);
|
|
243
|
+
increment(payload.facets.modelAttachments.byMediaType, "application/json", 1);
|
|
244
|
+
increment(payload.facets.modelAttachments.byStatus, "active", 1);
|
|
245
|
+
if (input.previousStatus && input.nextStatus && input.previousStatus !== input.nextStatus) {
|
|
246
|
+
increment(payload.assignmentStatusCounts, input.previousStatus, -1);
|
|
247
|
+
increment(payload.assignmentStatusCounts, input.nextStatus, 1);
|
|
248
|
+
increment(payload.facets.assignments.byStatus, input.previousStatus, -1);
|
|
249
|
+
increment(payload.facets.assignments.byStatus, input.nextStatus, 1);
|
|
250
|
+
if (input.assignmentTypeKey) {
|
|
251
|
+
if (!payload.facets.assignments.statusByType[input.assignmentTypeKey]) payload.facets.assignments.statusByType[input.assignmentTypeKey] = {};
|
|
252
|
+
increment(payload.facets.assignments.statusByType[input.assignmentTypeKey], input.previousStatus, -1);
|
|
253
|
+
increment(payload.facets.assignments.statusByType[input.assignmentTypeKey], input.nextStatus, 1);
|
|
254
|
+
}
|
|
255
|
+
const sectionKey = input.sectionKey || "unsectioned";
|
|
256
|
+
if (!payload.facets.assignments.statusBySection[sectionKey]) payload.facets.assignments.statusBySection[sectionKey] = {};
|
|
257
|
+
increment(payload.facets.assignments.statusBySection[sectionKey], input.previousStatus, -1);
|
|
258
|
+
increment(payload.facets.assignments.statusBySection[sectionKey], input.nextStatus, 1);
|
|
259
|
+
}
|
|
260
|
+
await upsertNewsroomSummaryPayload(client, payload, response.data, input.now);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function getAssignmentContext(event: Parameters<Schema["getAssignmentContext"]["functionHandler"]>[0]) {
|
|
264
|
+
const client = await getDataClient();
|
|
265
|
+
const assignmentId = normalizeRequiredString(event.arguments.assignmentId, "assignmentId");
|
|
266
|
+
const assignment = await getRequiredRecord(client.models.Assignment, assignmentId, "Assignment");
|
|
267
|
+
return buildAssignmentContext(client, assignment);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function listAssignmentQueue(event: Parameters<Schema["listAssignmentQueue"]["functionHandler"]>[0]) {
|
|
271
|
+
const client = await getDataClient();
|
|
272
|
+
const queueKey = normalizeRequiredString(event.arguments.queueKey, "queueKey");
|
|
273
|
+
const status = normalizeOptionalString(event.arguments.status) ?? "open";
|
|
274
|
+
const queueStatusKey = `${queueKey}#${status}`;
|
|
275
|
+
const model = client.models.Assignment as any;
|
|
276
|
+
const assignments: any[] = [];
|
|
277
|
+
let nextToken: string | null | undefined;
|
|
278
|
+
do {
|
|
279
|
+
const response: DataClientResult<any[]> = await model.listAssignmentsByQueueStatusAndPriority(
|
|
280
|
+
{ queueStatusKey },
|
|
281
|
+
{ limit: 100, nextToken },
|
|
282
|
+
);
|
|
283
|
+
assertNoDataErrors(response.errors, "list Assignment queue");
|
|
284
|
+
assignments.push(...(response.data ?? []));
|
|
285
|
+
nextToken = response.nextToken;
|
|
286
|
+
} while (nextToken);
|
|
287
|
+
assignments.sort((left, right) => assignmentSortKey(left).localeCompare(assignmentSortKey(right)));
|
|
288
|
+
const limit = Math.max(1, Math.min(100, Number(event.arguments.limit ?? 25)));
|
|
289
|
+
const contexts = [];
|
|
290
|
+
for (const assignment of assignments.slice(0, limit)) contexts.push(await buildAssignmentContext(client, assignment));
|
|
291
|
+
return contexts;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function listAssignmentsForObject(event: Parameters<Schema["listAssignmentsForObject"]["functionHandler"]>[0]) {
|
|
295
|
+
const client = await getDataClient();
|
|
296
|
+
const objectKind = normalizeRequiredString(event.arguments.objectKind, "objectKind");
|
|
297
|
+
const objectLineageId = normalizeRequiredString(event.arguments.objectLineageId, "objectLineageId");
|
|
298
|
+
const status = normalizeOptionalString(event.arguments.status);
|
|
299
|
+
const objectStateKey = semanticStateKey(objectKind, objectLineageId);
|
|
300
|
+
const relations = await listRelationPages(client, "listSemanticRelationsByObjectState", { objectStateKey });
|
|
301
|
+
const assignmentIds = Array.from(new Set(relations
|
|
302
|
+
.filter((relation) => relation.relationState === "current" && relation.subjectKind === "assignment")
|
|
303
|
+
.map((relation) => relation.subjectId)
|
|
304
|
+
.filter(Boolean)));
|
|
305
|
+
const assignments = [];
|
|
306
|
+
for (const assignmentId of assignmentIds) {
|
|
307
|
+
const response = await client.models.Assignment.get({ id: assignmentId });
|
|
308
|
+
assertNoDataErrors(response.errors, `get Assignment ${assignmentId}`);
|
|
309
|
+
if (response.data && (!status || response.data.status === status)) assignments.push(response.data);
|
|
310
|
+
}
|
|
311
|
+
assignments.sort((left, right) => assignmentSortKey(left).localeCompare(assignmentSortKey(right)));
|
|
312
|
+
const limit = Math.max(1, Math.min(100, Number(event.arguments.limit ?? 25)));
|
|
313
|
+
const contexts = [];
|
|
314
|
+
for (const assignment of assignments.slice(0, limit)) contexts.push(await buildAssignmentContext(client, assignment));
|
|
315
|
+
return contexts;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function buildAssignmentContext(client: DataClient, assignment: any) {
|
|
319
|
+
const [relations, events] = await Promise.all([
|
|
320
|
+
listRelationPages(client, "listSemanticRelationsBySubjectState", { subjectStateKey: semanticStateKey("assignment", assignment.id) }),
|
|
321
|
+
listAssignmentEventPages(client, assignment.id),
|
|
322
|
+
]);
|
|
323
|
+
const targets = relations
|
|
324
|
+
.filter((relation) => relation.relationState === "current")
|
|
325
|
+
.map((relation) => ({
|
|
326
|
+
kind: relation.objectKind,
|
|
327
|
+
id: relation.objectId,
|
|
328
|
+
lineageId: relation.objectLineageId,
|
|
329
|
+
label: targetLabelFromRelation(relation),
|
|
330
|
+
detail: relation.predicate,
|
|
331
|
+
}));
|
|
332
|
+
const rootDeskCategory = await resolveRootDeskCategory(client, assignment, relations);
|
|
333
|
+
const doctrine = await buildAssignmentDoctrineContext(client, rootDeskCategory);
|
|
334
|
+
return {
|
|
335
|
+
assignment,
|
|
336
|
+
doctrine,
|
|
337
|
+
targets,
|
|
338
|
+
events,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function buildAssignmentDoctrineContext(client: DataClient, rootDeskCategory: any | null): Promise<any[]> {
|
|
343
|
+
const definitions = [
|
|
344
|
+
...PUBLICATION_DOCTRINE_DEFINITIONS,
|
|
345
|
+
...(rootDeskCategory ? [
|
|
346
|
+
{
|
|
347
|
+
scope: "desk",
|
|
348
|
+
kind: "mission",
|
|
349
|
+
label: "Desk Mission",
|
|
350
|
+
slug: deskDoctrineSlug(rootDeskCategory.categoryKey, "mission"),
|
|
351
|
+
categoryKey: rootDeskCategory.categoryKey,
|
|
352
|
+
categoryLineageId: rootDeskCategory.lineageId ?? rootDeskCategory.id,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
scope: "desk",
|
|
356
|
+
kind: "policy",
|
|
357
|
+
label: "Desk Policies",
|
|
358
|
+
slug: deskDoctrineSlug(rootDeskCategory.categoryKey, "policy"),
|
|
359
|
+
categoryKey: rootDeskCategory.categoryKey,
|
|
360
|
+
categoryLineageId: rootDeskCategory.lineageId ?? rootDeskCategory.id,
|
|
361
|
+
},
|
|
362
|
+
] : []),
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
const context = [];
|
|
366
|
+
for (const definition of definitions) {
|
|
367
|
+
const record = await getDoctrineRecordBySlug(client, definition.slug);
|
|
368
|
+
if (!record) continue;
|
|
369
|
+
context.push({
|
|
370
|
+
scope: definition.scope,
|
|
371
|
+
kind: definition.kind,
|
|
372
|
+
label: definition.label,
|
|
373
|
+
slug: definition.slug,
|
|
374
|
+
body: compactStringArray(record.body),
|
|
375
|
+
categoryKey: "categoryKey" in definition ? definition.categoryKey : null,
|
|
376
|
+
categoryLineageId: "categoryLineageId" in definition ? definition.categoryLineageId : null,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return context;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function resolveRootDeskCategory(client: DataClient, assignment: any, relations: any[]): Promise<any | null> {
|
|
383
|
+
const categoryLineageId = assignmentCategoryLineageIdFromRelations(relations);
|
|
384
|
+
if (categoryLineageId) {
|
|
385
|
+
const category = await getCurrentCategoryByLineage(client, categoryLineageId);
|
|
386
|
+
return category ? resolveRootCategory(client, category) : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const categoryKey = normalizeOptionalString(assignment.primaryFocusCategoryKey);
|
|
390
|
+
if (categoryKey && assignment.categorySetId) {
|
|
391
|
+
const category = await getCurrentCategoryBySetAndKey(client, assignment.categorySetId, categoryKey);
|
|
392
|
+
return category ? resolveRootCategory(client, category) : null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function assignmentCategoryLineageIdFromRelations(relations: any[]): string | null {
|
|
399
|
+
const direct = relations.find((relation) => (
|
|
400
|
+
relation.relationState === "current"
|
|
401
|
+
&& relation.objectKind === "category"
|
|
402
|
+
&& relation.predicate === "requests_work_on"
|
|
403
|
+
)) ?? relations.find((relation) => (
|
|
404
|
+
relation.relationState === "current"
|
|
405
|
+
&& relation.objectKind === "category"
|
|
406
|
+
));
|
|
407
|
+
return normalizeOptionalString(direct?.objectLineageId);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function resolveRootCategory(client: DataClient, category: any): Promise<any | null> {
|
|
411
|
+
let current = category;
|
|
412
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
413
|
+
const parentCategoryKey = normalizeOptionalString(current?.parentCategoryKey);
|
|
414
|
+
if (!parentCategoryKey) return current;
|
|
415
|
+
const parent = await getCurrentCategoryBySetAndKey(client, current.categorySetId, parentCategoryKey);
|
|
416
|
+
if (!parent) return current;
|
|
417
|
+
current = parent;
|
|
418
|
+
}
|
|
419
|
+
return current;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function getCurrentCategoryByLineage(client: DataClient, lineageId: string): Promise<any | null> {
|
|
423
|
+
const model = client.models.Category as any;
|
|
424
|
+
const query = model.listCategoriesByLineageAndVersion;
|
|
425
|
+
if (typeof query !== "function") return null;
|
|
426
|
+
const categories: any[] = [];
|
|
427
|
+
let nextToken: string | null | undefined;
|
|
428
|
+
do {
|
|
429
|
+
const response: DataClientResult<any[]> = await query({ lineageId }, { limit: 100, nextToken });
|
|
430
|
+
assertNoDataErrors(response.errors, "list Category lineage");
|
|
431
|
+
categories.push(...(response.data ?? []));
|
|
432
|
+
nextToken = response.nextToken;
|
|
433
|
+
} while (nextToken);
|
|
434
|
+
return selectCurrentVersion(categories);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function getCurrentCategoryBySetAndKey(client: DataClient, categorySetId: string, categoryKey: string): Promise<any | null> {
|
|
438
|
+
const model = client.models.Category as any;
|
|
439
|
+
const query = model.listCategoriesBySetAndKey;
|
|
440
|
+
if (typeof query !== "function") return null;
|
|
441
|
+
const categories: any[] = [];
|
|
442
|
+
let nextToken: string | null | undefined;
|
|
443
|
+
do {
|
|
444
|
+
const response: DataClientResult<any[]> = await query({ categorySetId, categoryKey }, { limit: 100, nextToken });
|
|
445
|
+
assertNoDataErrors(response.errors, "list Category set/key");
|
|
446
|
+
categories.push(...(response.data ?? []));
|
|
447
|
+
nextToken = response.nextToken;
|
|
448
|
+
} while (nextToken);
|
|
449
|
+
return selectCurrentVersion(categories);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function getDoctrineRecordBySlug(client: DataClient, slug: string): Promise<any | null> {
|
|
453
|
+
const model = client.models.Item as any;
|
|
454
|
+
const query = model.itemBySlug;
|
|
455
|
+
if (typeof query !== "function") return null;
|
|
456
|
+
const records: any[] = [];
|
|
457
|
+
let nextToken: string | null | undefined;
|
|
458
|
+
do {
|
|
459
|
+
const response: DataClientResult<any[]> = await query({ slug }, { limit: 100, nextToken });
|
|
460
|
+
assertNoDataErrors(response.errors, "query Item by slug");
|
|
461
|
+
records.push(...(response.data ?? []));
|
|
462
|
+
nextToken = response.nextToken;
|
|
463
|
+
} while (nextToken);
|
|
464
|
+
return records.find((record) => record.type === "doctrine" && record.status === "private")
|
|
465
|
+
?? records.find((record) => record.type === "doctrine")
|
|
466
|
+
?? null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function selectCurrentVersion(records: any[]): any | null {
|
|
470
|
+
return [...records]
|
|
471
|
+
.filter((record) => record && record.status !== "deprecated")
|
|
472
|
+
.sort((left, right) => {
|
|
473
|
+
const stateDiff = versionStateRank(left.versionState) - versionStateRank(right.versionState);
|
|
474
|
+
if (stateDiff !== 0) return stateDiff;
|
|
475
|
+
return Number(right.versionNumber ?? 0) - Number(left.versionNumber ?? 0);
|
|
476
|
+
})[0] ?? null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function versionStateRank(versionState: unknown): number {
|
|
480
|
+
if (versionState === "current") return 0;
|
|
481
|
+
if (versionState === "draft") return 1;
|
|
482
|
+
if (versionState === "superseded") return 8;
|
|
483
|
+
return 5;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function deskDoctrineSlug(categoryKey: string, kind: "mission" | "policy"): string {
|
|
487
|
+
return `desk-doctrine-${safeDoctrineKey(categoryKey)}-${kind}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function safeDoctrineKey(value: string): string {
|
|
491
|
+
return value
|
|
492
|
+
.trim()
|
|
493
|
+
.toLowerCase()
|
|
494
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
495
|
+
.replace(/^-+|-+$/g, "")
|
|
496
|
+
|| "topic";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function compactStringArray(value: unknown): string[] {
|
|
500
|
+
if (!Array.isArray(value)) return [];
|
|
501
|
+
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function listRelationPages(client: DataClient, queryName: string, input: Record<string, unknown>): Promise<any[]> {
|
|
505
|
+
const model = client.models.SemanticRelation as any;
|
|
506
|
+
const query = model[queryName];
|
|
507
|
+
if (typeof query !== "function") throw new Error(`SemanticRelation query ${queryName} is not available.`);
|
|
508
|
+
const records: any[] = [];
|
|
509
|
+
let nextToken: string | null | undefined;
|
|
510
|
+
do {
|
|
511
|
+
const response: DataClientResult<any[]> = await query(input, { limit: 100, nextToken });
|
|
512
|
+
assertNoDataErrors(response.errors, queryName);
|
|
513
|
+
records.push(...(response.data ?? []));
|
|
514
|
+
nextToken = response.nextToken;
|
|
515
|
+
} while (nextToken);
|
|
516
|
+
return records;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function listAssignmentEventPages(client: DataClient, assignmentId: string): Promise<any[]> {
|
|
520
|
+
const model = client.models.AssignmentEvent as any;
|
|
521
|
+
const records: any[] = [];
|
|
522
|
+
let nextToken: string | null | undefined;
|
|
523
|
+
do {
|
|
524
|
+
const response: DataClientResult<any[]> = await model.listAssignmentEventsByAssignmentAndCreatedAt(
|
|
525
|
+
{ assignmentId },
|
|
526
|
+
{ limit: 100, nextToken },
|
|
527
|
+
);
|
|
528
|
+
assertNoDataErrors(response.errors, "list AssignmentEvent");
|
|
529
|
+
records.push(...(response.data ?? []));
|
|
530
|
+
nextToken = response.nextToken;
|
|
531
|
+
} while (nextToken);
|
|
532
|
+
return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function nextAssignmentUpdate(assignment: any, action: string, args: Record<string, unknown>, now: string): Record<string, unknown> {
|
|
536
|
+
if (action === "claim") {
|
|
537
|
+
if (FINAL_STATUSES.has(assignment.status)) throw new Error(`Cannot claim ${assignment.status} assignment ${assignment.id}.`);
|
|
538
|
+
const claimIdentity = resolveClaimIdentity(args);
|
|
539
|
+
if (activeClaimHeldByDifferentAssignee(assignment, claimIdentity.assigneeKey, now)) {
|
|
540
|
+
throw new Error(`Assignment ${assignment.id} is already claimed by ${assignment.assigneeKey}.`);
|
|
541
|
+
}
|
|
542
|
+
const sameAssignee = normalizeOptionalString(assignment.assigneeKey) === claimIdentity.assigneeKey;
|
|
543
|
+
const requestedClaimExpiresAt = resolveClaimExpiresAt(args, now);
|
|
544
|
+
const claimExpiresAt = requestedClaimExpiresAt ?? (sameAssignee ? normalizeOptionalString(assignment.claimExpiresAt) : null);
|
|
545
|
+
return {
|
|
546
|
+
status: "claimed",
|
|
547
|
+
queueStatusKey: `${assignment.queueKey}#claimed`,
|
|
548
|
+
sectionStatusKey: sectionStatusKey(assignment, "claimed"),
|
|
549
|
+
sectionQueueStatusKey: sectionQueueStatusKey(assignment, "claimed"),
|
|
550
|
+
assigneeType: claimIdentity.assigneeType,
|
|
551
|
+
assigneeId: claimIdentity.assigneeId,
|
|
552
|
+
assigneeKey: claimIdentity.assigneeKey,
|
|
553
|
+
claimedAt: sameAssignee && normalizeOptionalString(assignment.claimedAt) ? assignment.claimedAt : now,
|
|
554
|
+
claimExpiresAt,
|
|
555
|
+
updatedAt: now,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (action === "release") {
|
|
559
|
+
if (FINAL_STATUSES.has(assignment.status)) throw new Error(`Cannot release ${assignment.status} assignment ${assignment.id}.`);
|
|
560
|
+
return {
|
|
561
|
+
status: "open",
|
|
562
|
+
queueStatusKey: `${assignment.queueKey}#open`,
|
|
563
|
+
sectionStatusKey: sectionStatusKey(assignment, "open"),
|
|
564
|
+
sectionQueueStatusKey: sectionQueueStatusKey(assignment, "open"),
|
|
565
|
+
assigneeType: null,
|
|
566
|
+
assigneeId: null,
|
|
567
|
+
assigneeKey: null,
|
|
568
|
+
claimedAt: null,
|
|
569
|
+
claimExpiresAt: null,
|
|
570
|
+
updatedAt: now,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (action === "complete") {
|
|
574
|
+
if (assignment.status === "canceled") throw new Error(`Cannot complete canceled assignment ${assignment.id}.`);
|
|
575
|
+
return {
|
|
576
|
+
status: "completed",
|
|
577
|
+
queueStatusKey: `${assignment.queueKey}#completed`,
|
|
578
|
+
sectionStatusKey: sectionStatusKey(assignment, "completed"),
|
|
579
|
+
sectionQueueStatusKey: sectionQueueStatusKey(assignment, "completed"),
|
|
580
|
+
completedAt: now,
|
|
581
|
+
updatedAt: now,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (action === "cancel") {
|
|
585
|
+
if (assignment.status === "completed") throw new Error(`Cannot cancel completed assignment ${assignment.id}.`);
|
|
586
|
+
return {
|
|
587
|
+
status: "canceled",
|
|
588
|
+
queueStatusKey: `${assignment.queueKey}#canceled`,
|
|
589
|
+
sectionStatusKey: sectionStatusKey(assignment, "canceled"),
|
|
590
|
+
sectionQueueStatusKey: sectionQueueStatusKey(assignment, "canceled"),
|
|
591
|
+
canceledAt: now,
|
|
592
|
+
updatedAt: now,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (action === "reopen") {
|
|
596
|
+
return {
|
|
597
|
+
status: "open",
|
|
598
|
+
queueStatusKey: `${assignment.queueKey}#open`,
|
|
599
|
+
sectionStatusKey: sectionStatusKey(assignment, "open"),
|
|
600
|
+
sectionQueueStatusKey: sectionQueueStatusKey(assignment, "open"),
|
|
601
|
+
completedAt: null,
|
|
602
|
+
canceledAt: null,
|
|
603
|
+
updatedAt: now,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
throw new Error(`Unsupported assignment action ${action}.`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function assignmentExecutionMode(assignmentTypeKey: unknown): "immediate" | "queued" {
|
|
610
|
+
return IMMEDIATE_ASSIGNMENT_TYPES.has(normalizeOptionalString(assignmentTypeKey) ?? "") ? "immediate" : "queued";
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function executeImmediateClaimedAssignment(
|
|
614
|
+
client: DataClient,
|
|
615
|
+
input: {
|
|
616
|
+
assignment: any;
|
|
617
|
+
actorSub: string | null;
|
|
618
|
+
actorLabel: string | null;
|
|
619
|
+
now: string;
|
|
620
|
+
note: string | null;
|
|
621
|
+
trigger: "claim" | "retry";
|
|
622
|
+
},
|
|
623
|
+
): Promise<{ status: "completed" | "open"; eventId: string }> {
|
|
624
|
+
const assignmentId = normalizeRequiredString(input.assignment.id, "assignment.id");
|
|
625
|
+
const assignmentTypeKey = normalizeOptionalString(input.assignment.assignmentTypeKey) ?? "unknown";
|
|
626
|
+
const queueKey = normalizeOptionalString(input.assignment.queueKey) ?? `${assignmentTypeKey}#unknown`;
|
|
627
|
+
const startEventId = `assignment-event-${assignmentId}-execution-start-${input.now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 6)}`;
|
|
628
|
+
await requireDataResult(
|
|
629
|
+
client.models.AssignmentEvent.create({
|
|
630
|
+
id: startEventId,
|
|
631
|
+
assignmentId,
|
|
632
|
+
assignmentTypeKey,
|
|
633
|
+
queueKey,
|
|
634
|
+
eventType: "execution_started",
|
|
635
|
+
fromStatus: "claimed",
|
|
636
|
+
toStatus: "claimed",
|
|
637
|
+
actorSub: input.actorSub,
|
|
638
|
+
actorLabel: input.actorLabel,
|
|
639
|
+
note: input.note ?? `${input.trigger} immediate execution`,
|
|
640
|
+
createdAt: input.now,
|
|
641
|
+
}),
|
|
642
|
+
`create AssignmentEvent ${startEventId}`,
|
|
643
|
+
);
|
|
644
|
+
let executionResult: { runId?: string | null; summary: string; output?: unknown } = { summary: "No executor response." };
|
|
645
|
+
try {
|
|
646
|
+
executionResult = await runImmediateAssignmentExecutor(client, input.assignment, input.now);
|
|
647
|
+
const completedAt = new Date().toISOString();
|
|
648
|
+
await requireDataResult(client.models.Assignment.update({
|
|
649
|
+
id: assignmentId,
|
|
650
|
+
status: "completed",
|
|
651
|
+
queueStatusKey: `${queueKey}#completed`,
|
|
652
|
+
sectionStatusKey: sectionStatusKey(input.assignment, "completed"),
|
|
653
|
+
sectionQueueStatusKey: sectionQueueStatusKey(input.assignment, "completed"),
|
|
654
|
+
completedAt,
|
|
655
|
+
updatedAt: completedAt,
|
|
656
|
+
}), `complete immediate Assignment ${assignmentId}`);
|
|
657
|
+
const completeEventId = `assignment-event-${assignmentId}-execution-complete-${completedAt.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 6)}`;
|
|
658
|
+
await requireDataResult(
|
|
659
|
+
client.models.AssignmentEvent.create({
|
|
660
|
+
id: completeEventId,
|
|
661
|
+
assignmentId,
|
|
662
|
+
assignmentTypeKey,
|
|
663
|
+
queueKey,
|
|
664
|
+
eventType: "execution_completed",
|
|
665
|
+
fromStatus: "claimed",
|
|
666
|
+
toStatus: "completed",
|
|
667
|
+
actorSub: input.actorSub,
|
|
668
|
+
actorLabel: input.actorLabel,
|
|
669
|
+
note: executionResult.summary,
|
|
670
|
+
createdAt: completedAt,
|
|
671
|
+
}),
|
|
672
|
+
`create AssignmentEvent ${completeEventId}`,
|
|
673
|
+
);
|
|
674
|
+
await putJsonModelPayload(
|
|
675
|
+
client as any,
|
|
676
|
+
{ ownerKind: "assignmentEvent", ownerId: completeEventId, ownerLineageId: completeEventId },
|
|
677
|
+
"result",
|
|
678
|
+
"result",
|
|
679
|
+
{
|
|
680
|
+
trigger: input.trigger,
|
|
681
|
+
runId: executionResult.runId ?? null,
|
|
682
|
+
summary: executionResult.summary,
|
|
683
|
+
output: executionResult.output ?? null,
|
|
684
|
+
},
|
|
685
|
+
{ filename: "result.json", now: completedAt },
|
|
686
|
+
);
|
|
687
|
+
await updateNewsroomSummaryForAssignmentAction(client, {
|
|
688
|
+
assignmentTypeKey,
|
|
689
|
+
sectionKey: assignmentSectionKey(input.assignment),
|
|
690
|
+
previousStatus: "claimed",
|
|
691
|
+
nextStatus: "completed",
|
|
692
|
+
now: completedAt,
|
|
693
|
+
});
|
|
694
|
+
return { status: "completed", eventId: completeEventId };
|
|
695
|
+
} catch (error) {
|
|
696
|
+
const failedAt = new Date().toISOString();
|
|
697
|
+
const errorMessage = error instanceof Error ? error.message : "Immediate execution failed.";
|
|
698
|
+
await requireDataResult(client.models.Assignment.update({
|
|
699
|
+
id: assignmentId,
|
|
700
|
+
status: "open",
|
|
701
|
+
queueStatusKey: `${queueKey}#open`,
|
|
702
|
+
sectionStatusKey: sectionStatusKey(input.assignment, "open"),
|
|
703
|
+
sectionQueueStatusKey: sectionQueueStatusKey(input.assignment, "open"),
|
|
704
|
+
assigneeType: null,
|
|
705
|
+
assigneeId: null,
|
|
706
|
+
assigneeKey: null,
|
|
707
|
+
claimedAt: null,
|
|
708
|
+
claimExpiresAt: null,
|
|
709
|
+
updatedAt: failedAt,
|
|
710
|
+
}), `reopen failed immediate Assignment ${assignmentId}`);
|
|
711
|
+
const failedEventId = `assignment-event-${assignmentId}-execution-failed-${failedAt.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 6)}`;
|
|
712
|
+
await requireDataResult(
|
|
713
|
+
client.models.AssignmentEvent.create({
|
|
714
|
+
id: failedEventId,
|
|
715
|
+
assignmentId,
|
|
716
|
+
assignmentTypeKey,
|
|
717
|
+
queueKey,
|
|
718
|
+
eventType: "execution_failed",
|
|
719
|
+
fromStatus: "claimed",
|
|
720
|
+
toStatus: "open",
|
|
721
|
+
actorSub: input.actorSub,
|
|
722
|
+
actorLabel: input.actorLabel,
|
|
723
|
+
note: errorMessage,
|
|
724
|
+
createdAt: failedAt,
|
|
725
|
+
}),
|
|
726
|
+
`create AssignmentEvent ${failedEventId}`,
|
|
727
|
+
);
|
|
728
|
+
await putJsonModelPayload(
|
|
729
|
+
client as any,
|
|
730
|
+
{ ownerKind: "assignmentEvent", ownerId: failedEventId, ownerLineageId: failedEventId },
|
|
731
|
+
"error",
|
|
732
|
+
"error",
|
|
733
|
+
{
|
|
734
|
+
trigger: input.trigger,
|
|
735
|
+
runId: executionResult.runId ?? null,
|
|
736
|
+
error: errorMessage,
|
|
737
|
+
},
|
|
738
|
+
{ filename: "error.json", now: failedAt },
|
|
739
|
+
);
|
|
740
|
+
await updateNewsroomSummaryForAssignmentAction(client, {
|
|
741
|
+
assignmentTypeKey,
|
|
742
|
+
sectionKey: assignmentSectionKey(input.assignment),
|
|
743
|
+
previousStatus: "claimed",
|
|
744
|
+
nextStatus: "open",
|
|
745
|
+
now: failedAt,
|
|
746
|
+
});
|
|
747
|
+
return { status: "open", eventId: failedEventId };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function runImmediateAssignmentExecutor(client: DataClient, assignment: any, now: string): Promise<{ runId?: string | null; summary: string; output?: unknown }> {
|
|
752
|
+
const assignmentTypeKey = normalizeOptionalString(assignment.assignmentTypeKey) ?? "";
|
|
753
|
+
if (assignmentTypeKey === "procedure.run") {
|
|
754
|
+
return await executeImmediateProcedureRun(client, assignment, now);
|
|
755
|
+
}
|
|
756
|
+
throw new Error(`No immediate executor is registered for ${assignmentTypeKey}.`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function executeImmediateProcedureRun(client: DataClient, assignment: any, now: string): Promise<{ runId?: string | null; summary: string; output?: unknown }> {
|
|
760
|
+
const runId = normalizeOptionalString(assignment.sourceSnapshotId);
|
|
761
|
+
if (!runId) throw new Error(`Assignment ${assignment.id} is missing sourceSnapshotId for procedure run execution.`);
|
|
762
|
+
const run = await getRequiredRecord((client.models as any).ProcedureRun, runId, "ProcedureRun");
|
|
763
|
+
const version = await getRequiredRecord((client.models as any).ProcedureVersion, normalizeRequiredString(run.procedureVersionId, "procedureVersionId"), "ProcedureVersion");
|
|
764
|
+
const definition = await getRequiredRecord((client.models as any).ProcedureDefinition, normalizeRequiredString(run.procedureId, "procedureId"), "ProcedureDefinition");
|
|
765
|
+
const attempt = normalizeOptionalPositiveInteger(run.attempt) ?? 1;
|
|
766
|
+
const inputPayload = parseJsonObject(run.normalizedInput) ?? parseJsonObject(run.input) ?? {};
|
|
767
|
+
const startedAt = run.startedAt ?? now;
|
|
768
|
+
try {
|
|
769
|
+
const procedureKey = normalizeOptionalString(definition.procedureKey) ?? "unknown";
|
|
770
|
+
const procedureKind = resolveProcedureKind(version, definition);
|
|
771
|
+
const payload = executeSeededProcedurePayload({
|
|
772
|
+
procedureKey,
|
|
773
|
+
procedureKind,
|
|
774
|
+
assignmentId: normalizeOptionalString(assignment.id),
|
|
775
|
+
runId: run.id,
|
|
776
|
+
attempt,
|
|
777
|
+
now,
|
|
778
|
+
input: inputPayload,
|
|
779
|
+
});
|
|
780
|
+
const executionOutput = {
|
|
781
|
+
procedureKey,
|
|
782
|
+
procedureKind,
|
|
783
|
+
versionNumber: Number(version.versionNumber ?? 1),
|
|
784
|
+
attempt,
|
|
785
|
+
executedAt: now,
|
|
786
|
+
mode: "immediate",
|
|
787
|
+
input: inputPayload,
|
|
788
|
+
...payload,
|
|
789
|
+
};
|
|
790
|
+
const finishedAt = new Date().toISOString();
|
|
791
|
+
await requireDataResult((client.models as any).ProcedureRun.update({
|
|
792
|
+
id: run.id,
|
|
793
|
+
runStatus: "completed",
|
|
794
|
+
startedAt,
|
|
795
|
+
finishedAt,
|
|
796
|
+
resultSummary: `Completed immediate run for ${definition.procedureKey} v${version.versionNumber}.`,
|
|
797
|
+
errorSummary: null,
|
|
798
|
+
output: toAwsJson(executionOutput),
|
|
799
|
+
error: null,
|
|
800
|
+
attempt,
|
|
801
|
+
}), `update ProcedureRun ${run.id}`);
|
|
802
|
+
return {
|
|
803
|
+
runId: run.id,
|
|
804
|
+
summary: `Procedure ${definition.procedureKey} completed.`,
|
|
805
|
+
output: executionOutput,
|
|
806
|
+
};
|
|
807
|
+
} catch (error) {
|
|
808
|
+
const finishedAt = new Date().toISOString();
|
|
809
|
+
const errorMessage = error instanceof Error ? error.message : "Procedure execution failed.";
|
|
810
|
+
await requireDataResult((client.models as any).ProcedureRun.update({
|
|
811
|
+
id: run.id,
|
|
812
|
+
runStatus: "failed",
|
|
813
|
+
startedAt,
|
|
814
|
+
finishedAt,
|
|
815
|
+
resultSummary: null,
|
|
816
|
+
errorSummary: errorMessage,
|
|
817
|
+
output: null,
|
|
818
|
+
error: toAwsJson({
|
|
819
|
+
message: errorMessage,
|
|
820
|
+
procedureKey: normalizeOptionalString(definition.procedureKey),
|
|
821
|
+
procedureVersionId: normalizeOptionalString(version.id),
|
|
822
|
+
}),
|
|
823
|
+
attempt,
|
|
824
|
+
}), `update failed ProcedureRun ${run.id}`);
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function resolveProcedureKind(version: any, definition: any): string {
|
|
830
|
+
const source = String(version?.tactusSource ?? "");
|
|
831
|
+
const marker = source.match(/papyrus-procedure-kind:\s*([a-z0-9._-]+)/i)?.[1];
|
|
832
|
+
if (marker) return marker;
|
|
833
|
+
return normalizeOptionalString(definition?.procedureKey) ?? "unknown";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function executeSeededProcedurePayload(input: {
|
|
837
|
+
procedureKey: string;
|
|
838
|
+
procedureKind: string;
|
|
839
|
+
assignmentId: string | null;
|
|
840
|
+
runId: string;
|
|
841
|
+
attempt: number;
|
|
842
|
+
now: string;
|
|
843
|
+
input: Record<string, unknown>;
|
|
844
|
+
}): Record<string, unknown> {
|
|
845
|
+
if (input.procedureKind === "newsroom.research.explorer") {
|
|
846
|
+
return {
|
|
847
|
+
research_packet: buildLambdaResearchPacket(input),
|
|
848
|
+
retry_count: 0,
|
|
849
|
+
validation_failures: [],
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (input.procedureKind === "newsroom.reporting.context") {
|
|
853
|
+
return {
|
|
854
|
+
reporting_context_packet: buildLambdaReportingPacket(input),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
if (input.procedureKind === "ingestion.reference.register") {
|
|
858
|
+
const registerResult = runIngestionReferenceRegisterViaCli(input);
|
|
859
|
+
return {
|
|
860
|
+
result: {
|
|
861
|
+
ok: true,
|
|
862
|
+
kind: "ingestion.reference.register",
|
|
863
|
+
assignmentId: input.assignmentId,
|
|
864
|
+
runId: input.runId,
|
|
865
|
+
register: registerResult,
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
throw new Error(`Unsupported seeded procedure kind '${input.procedureKind}' for ${input.procedureKey}.`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function runIngestionReferenceRegisterViaCli(input: {
|
|
873
|
+
procedureKind: string;
|
|
874
|
+
input: Record<string, unknown>;
|
|
875
|
+
}): Record<string, unknown> {
|
|
876
|
+
const externalItemId = normalizeRequiredString(
|
|
877
|
+
input.input.externalItemId ?? input.input.external_item_id,
|
|
878
|
+
"input.externalItemId",
|
|
879
|
+
);
|
|
880
|
+
const sourceUri = normalizeRequiredString(
|
|
881
|
+
input.input.sourceUri ?? input.input.source_uri,
|
|
882
|
+
"input.sourceUri",
|
|
883
|
+
);
|
|
884
|
+
const title = normalizeOptionalString(input.input.title) ?? "Untitled source";
|
|
885
|
+
const corpusKey = normalizeOptionalString(input.input.corpusKey ?? input.input.corpus_key) ?? "AI-ML-research";
|
|
886
|
+
const ingestionRationale = normalizeOptionalString(input.input.ingestionRationale ?? input.input.ingestion_rationale)
|
|
887
|
+
?? `Registered from procedure ingestion.reference.register for ${externalItemId}.`;
|
|
888
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "papyrus-ingestion-reference-register-"));
|
|
889
|
+
const catalogPath = path.join(tempDir, "catalog.json");
|
|
890
|
+
writeFileSync(
|
|
891
|
+
catalogPath,
|
|
892
|
+
JSON.stringify({
|
|
893
|
+
schema_version: 1,
|
|
894
|
+
items: [
|
|
895
|
+
{
|
|
896
|
+
id: externalItemId,
|
|
897
|
+
item_id: externalItemId,
|
|
898
|
+
external_item_id: externalItemId,
|
|
899
|
+
title,
|
|
900
|
+
source_uri: sourceUri,
|
|
901
|
+
ingestion_rationale: ingestionRationale,
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
}, null, 2),
|
|
905
|
+
"utf8",
|
|
906
|
+
);
|
|
907
|
+
try {
|
|
908
|
+
const register = spawnSync(
|
|
909
|
+
"npm",
|
|
910
|
+
[
|
|
911
|
+
"run",
|
|
912
|
+
"content",
|
|
913
|
+
"--",
|
|
914
|
+
"references",
|
|
915
|
+
"register-catalog",
|
|
916
|
+
"--catalog",
|
|
917
|
+
catalogPath,
|
|
918
|
+
"--corpus-key",
|
|
919
|
+
corpusKey,
|
|
920
|
+
"--status",
|
|
921
|
+
"pending",
|
|
922
|
+
"--vector-sync",
|
|
923
|
+
"false",
|
|
924
|
+
"--url-text",
|
|
925
|
+
"false",
|
|
926
|
+
"--metadata-from-text",
|
|
927
|
+
"false",
|
|
928
|
+
"--apply",
|
|
929
|
+
],
|
|
930
|
+
{ encoding: "utf8" },
|
|
931
|
+
);
|
|
932
|
+
if (register.status !== 0) {
|
|
933
|
+
throw new Error((register.stderr || register.stdout || "register-catalog failed").trim());
|
|
934
|
+
}
|
|
935
|
+
const fetch = spawnSync(
|
|
936
|
+
"npm",
|
|
937
|
+
[
|
|
938
|
+
"run",
|
|
939
|
+
"content",
|
|
940
|
+
"--",
|
|
941
|
+
"references",
|
|
942
|
+
"fetch-url-text",
|
|
943
|
+
"--corpus-key",
|
|
944
|
+
corpusKey,
|
|
945
|
+
"--external-item-id",
|
|
946
|
+
externalItemId,
|
|
947
|
+
"--status",
|
|
948
|
+
"all",
|
|
949
|
+
"--max-count",
|
|
950
|
+
"1",
|
|
951
|
+
"--apply",
|
|
952
|
+
],
|
|
953
|
+
{ encoding: "utf8" },
|
|
954
|
+
);
|
|
955
|
+
if (fetch.status !== 0) {
|
|
956
|
+
throw new Error((fetch.stderr || fetch.stdout || "fetch-url-text failed").trim());
|
|
957
|
+
}
|
|
958
|
+
const generate = spawnSync(
|
|
959
|
+
"npm",
|
|
960
|
+
[
|
|
961
|
+
"run",
|
|
962
|
+
"content",
|
|
963
|
+
"--",
|
|
964
|
+
"references",
|
|
965
|
+
"generate-metadata-from-text",
|
|
966
|
+
"--corpus-key",
|
|
967
|
+
corpusKey,
|
|
968
|
+
"--external-item-id",
|
|
969
|
+
externalItemId,
|
|
970
|
+
"--status",
|
|
971
|
+
"all",
|
|
972
|
+
"--model",
|
|
973
|
+
"gpt-5.4-nano",
|
|
974
|
+
"--max-count",
|
|
975
|
+
"1",
|
|
976
|
+
"--apply",
|
|
977
|
+
],
|
|
978
|
+
{ encoding: "utf8" },
|
|
979
|
+
);
|
|
980
|
+
if (generate.status !== 0) {
|
|
981
|
+
throw new Error((generate.stderr || generate.stdout || "generate-metadata-from-text failed").trim());
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
externalItemId,
|
|
985
|
+
sourceUri,
|
|
986
|
+
corpusKey,
|
|
987
|
+
registerExitCode: register.status ?? 0,
|
|
988
|
+
fetchExitCode: fetch.status ?? 0,
|
|
989
|
+
generateExitCode: generate.status ?? 0,
|
|
990
|
+
fetchedTextChanges: parseTsvMetric(fetch.stdout, "references", "fetch-url-text", "changes"),
|
|
991
|
+
fetchedTextFailures: parseTsvMetric(fetch.stdout, "references", "fetch-url-text", "failures"),
|
|
992
|
+
metadataAttempted: parseTsvMetric(generate.stdout, "references", "generate-metadata-from-text", "attempted"),
|
|
993
|
+
metadataGenerated: parseTsvMetric(generate.stdout, "references", "generate-metadata-from-text", "generated"),
|
|
994
|
+
metadataSkippedMissingText: parseTsvMetric(generate.stdout, "references", "generate-metadata-from-text", "skipped-missing-text"),
|
|
995
|
+
metadataGenerationFailures: parseTsvMetric(generate.stdout, "references", "generate-metadata-from-text", "generation-failures"),
|
|
996
|
+
};
|
|
997
|
+
} finally {
|
|
998
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function parseTsvMetric(output: string, section: string, command: string, field: string): number | null {
|
|
1003
|
+
const lines = (output || "").split(/\r?\n/);
|
|
1004
|
+
for (const line of lines) {
|
|
1005
|
+
const parts = line.trim().split("\t");
|
|
1006
|
+
if (parts.length < 4) continue;
|
|
1007
|
+
if (parts[0] === section && parts[1] === command && parts[2] === field) {
|
|
1008
|
+
const value = Number(parts[3]);
|
|
1009
|
+
return Number.isFinite(value) ? value : null;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function buildLambdaResearchPacket(input: {
|
|
1016
|
+
procedureKey: string;
|
|
1017
|
+
assignmentId: string | null;
|
|
1018
|
+
runId: string;
|
|
1019
|
+
now: string;
|
|
1020
|
+
input: Record<string, unknown>;
|
|
1021
|
+
}): Record<string, unknown> {
|
|
1022
|
+
const researchMode = normalizeOptionalString(input.input.research_mode) ?? "source_discovery";
|
|
1023
|
+
const corpusKey = normalizeOptionalString(input.input.corpus_key);
|
|
1024
|
+
const question = normalizeOptionalString(input.input.research_questions);
|
|
1025
|
+
const blockedReason = "Lambda immediate seeded procedure executed without external Tactus tool calls.";
|
|
1026
|
+
return {
|
|
1027
|
+
summary: `Immediate research packet generated for ${input.assignmentId ?? "assignment"}.`,
|
|
1028
|
+
corpus_key: corpusKey,
|
|
1029
|
+
research_mode: researchMode,
|
|
1030
|
+
queries: question ? [question] : [],
|
|
1031
|
+
evidence_item_ids: [],
|
|
1032
|
+
source_snapshots: [],
|
|
1033
|
+
proposed_references: [],
|
|
1034
|
+
source_discovery: {
|
|
1035
|
+
source_snapshots: [],
|
|
1036
|
+
proposed_references: [],
|
|
1037
|
+
blocked_reason: blockedReason,
|
|
1038
|
+
},
|
|
1039
|
+
recommended_angle: "Gather accepted references before drafting reporting context.",
|
|
1040
|
+
open_questions: [
|
|
1041
|
+
"Which accepted references should anchor this angle?",
|
|
1042
|
+
],
|
|
1043
|
+
coverage_gaps: [
|
|
1044
|
+
"No external discovery executed in immediate Lambda seeded procedure mode.",
|
|
1045
|
+
],
|
|
1046
|
+
researchTrace: {
|
|
1047
|
+
recoveryPath: "lambda_immediate_seeded_procedure",
|
|
1048
|
+
runId: input.runId,
|
|
1049
|
+
executedAt: input.now,
|
|
1050
|
+
blockedReason,
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function buildLambdaReportingPacket(input: {
|
|
1056
|
+
assignmentId: string | null;
|
|
1057
|
+
runId: string;
|
|
1058
|
+
now: string;
|
|
1059
|
+
input: Record<string, unknown>;
|
|
1060
|
+
}): Record<string, unknown> {
|
|
1061
|
+
return {
|
|
1062
|
+
editor_recommendation: "hold",
|
|
1063
|
+
recommended_angle: "Collect stronger accepted-reference evidence before copywriting.",
|
|
1064
|
+
accepted_reference_ids: [],
|
|
1065
|
+
risk_flags: [
|
|
1066
|
+
"Immediate seeded Lambda procedure produced a placeholder reporting packet.",
|
|
1067
|
+
],
|
|
1068
|
+
coverage_gaps: [
|
|
1069
|
+
"Run full reporting procedure after confirming source research packet and accepted references.",
|
|
1070
|
+
],
|
|
1071
|
+
open_questions: [
|
|
1072
|
+
"What primary claim should this section make next?",
|
|
1073
|
+
],
|
|
1074
|
+
copywriter_brief: "Hold draft creation until reporting packet evidence is enriched.",
|
|
1075
|
+
source_research_assignment_id: normalizeOptionalString(input.input.source_research_assignment_id),
|
|
1076
|
+
source_research_packet_id: normalizeOptionalString(input.input.source_research_packet_id),
|
|
1077
|
+
source_research_packet_path: normalizeOptionalString(input.input.source_research_packet_path),
|
|
1078
|
+
run_id: input.runId,
|
|
1079
|
+
generated_at: input.now,
|
|
1080
|
+
assignment_id: input.assignmentId,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function assignmentSectionKey(assignment: any): string | null {
|
|
1085
|
+
return normalizeOptionalString(assignment?.sectionKey)
|
|
1086
|
+
?? normalizeOptionalString(assignment?.sectionId);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function sectionStatusKey(assignment: any, status: string): string | null {
|
|
1090
|
+
const sectionKey = assignmentSectionKey(assignment);
|
|
1091
|
+
return sectionKey ? `${sectionKey}#${status}` : null;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function sectionQueueStatusKey(assignment: any, status: string): string | null {
|
|
1095
|
+
const sectionKey = assignmentSectionKey(assignment);
|
|
1096
|
+
const queueKey = normalizeOptionalString(assignment?.queueKey);
|
|
1097
|
+
return sectionKey && queueKey ? `${sectionKey}#${queueKey}#${status}` : null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function resolveClaimIdentity(args: Record<string, unknown>): { assigneeType: string | null; assigneeId: string | null; assigneeKey: string } {
|
|
1101
|
+
const explicitAssigneeKey = normalizeOptionalString(args.assigneeKey);
|
|
1102
|
+
if (explicitAssigneeKey) {
|
|
1103
|
+
return {
|
|
1104
|
+
assigneeType: normalizeOptionalString(args.assigneeType),
|
|
1105
|
+
assigneeId: normalizeOptionalString(args.assigneeId),
|
|
1106
|
+
assigneeKey: explicitAssigneeKey,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
const assigneeType = normalizeOptionalString(args.assigneeType) ?? "user";
|
|
1110
|
+
const assigneeId = normalizeOptionalString(args.assigneeId) ?? normalizeOptionalString(args.actorSub) ?? "unknown";
|
|
1111
|
+
return {
|
|
1112
|
+
assigneeType,
|
|
1113
|
+
assigneeId,
|
|
1114
|
+
assigneeKey: `${assigneeType}#${assigneeId}`,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function activeClaimHeldByDifferentAssignee(assignment: any, requestedAssigneeKey: string, now: string): boolean {
|
|
1119
|
+
if (assignment.status !== "claimed") return false;
|
|
1120
|
+
const currentAssigneeKey = normalizeOptionalString(assignment.assigneeKey);
|
|
1121
|
+
if (!currentAssigneeKey || currentAssigneeKey === requestedAssigneeKey) return false;
|
|
1122
|
+
const claimExpiresAt = normalizeOptionalString(assignment.claimExpiresAt);
|
|
1123
|
+
if (!claimExpiresAt) return true;
|
|
1124
|
+
const expirationTime = Date.parse(claimExpiresAt);
|
|
1125
|
+
if (!Number.isFinite(expirationTime)) return true;
|
|
1126
|
+
return expirationTime > Date.parse(now);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function resolveClaimExpiresAt(args: Record<string, unknown>, now: string): string | null {
|
|
1130
|
+
const explicitExpiration = normalizeOptionalString(args.claimExpiresAt);
|
|
1131
|
+
if (explicitExpiration) {
|
|
1132
|
+
const expirationTime = Date.parse(explicitExpiration);
|
|
1133
|
+
if (!Number.isFinite(expirationTime)) throw new Error(`Invalid claimExpiresAt value ${explicitExpiration}.`);
|
|
1134
|
+
return new Date(expirationTime).toISOString();
|
|
1135
|
+
}
|
|
1136
|
+
const ttlSeconds = normalizeOptionalPositiveInteger(args.claimTtlSeconds);
|
|
1137
|
+
if (!ttlSeconds) return null;
|
|
1138
|
+
return new Date(Date.parse(now) + ttlSeconds * 1000).toISOString();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function normalizeOptionalPositiveInteger(value: unknown): number | null {
|
|
1142
|
+
if (value === null || value === undefined || value === "") return null;
|
|
1143
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
1144
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Expected a positive integer, got ${String(value)}.`);
|
|
1145
|
+
return parsed;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function assignmentActionFromField(fieldName: string): string {
|
|
1149
|
+
if (fieldName === "claimAssignment") return "claim";
|
|
1150
|
+
if (fieldName === "releaseAssignment") return "release";
|
|
1151
|
+
if (fieldName === "completeAssignment") return "complete";
|
|
1152
|
+
if (fieldName === "cancelAssignment") return "cancel";
|
|
1153
|
+
if (fieldName === "reopenAssignment") return "reopen";
|
|
1154
|
+
throw new Error(`Unsupported assignment action ${fieldName}.`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function getDataClient(): Promise<DataClient> {
|
|
1158
|
+
if (!clientPromise) {
|
|
1159
|
+
clientPromise = (async () => {
|
|
1160
|
+
const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(process.env as never);
|
|
1161
|
+
Amplify.configure(resourceConfig, libraryOptions);
|
|
1162
|
+
return generateClient<Schema>();
|
|
1163
|
+
})();
|
|
1164
|
+
}
|
|
1165
|
+
return clientPromise;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function getRequiredRecord(
|
|
1169
|
+
model: { get(input: { id: string }): Promise<{ data?: unknown | null; errors?: DataClientErrors }> },
|
|
1170
|
+
id: string,
|
|
1171
|
+
modelName: string,
|
|
1172
|
+
): Promise<any> {
|
|
1173
|
+
const response = await model.get({ id });
|
|
1174
|
+
assertNoDataErrors(response.errors, `get ${modelName}`);
|
|
1175
|
+
if (!response.data) throw new Error(`${modelName} ${id} was not found.`);
|
|
1176
|
+
return response.data;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async function requireDataResult<T>(promise: Promise<DataClientResult<T>>, operation: string): Promise<T> {
|
|
1180
|
+
const response = await promise;
|
|
1181
|
+
assertNoDataErrors(response.errors, operation);
|
|
1182
|
+
if (!response.data) throw new Error(`${operation} returned no data.`);
|
|
1183
|
+
return response.data;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function assertNoDataErrors(errors: DataClientErrors, operation: string): void {
|
|
1187
|
+
if (!errors?.length) return;
|
|
1188
|
+
throw new Error(`${operation} failed: ${errors.map(formatDataError).join("; ")}`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function formatDataError(error: { message?: string | null } | string | null): string {
|
|
1192
|
+
if (typeof error === "string") return error;
|
|
1193
|
+
return error?.message ?? "GraphQL data operation failed.";
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function semanticStateKey(kind: string, lineageId: string): string {
|
|
1197
|
+
return `${kind}#${lineageId}#current`;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function assignmentSortKey(assignment: any): string {
|
|
1201
|
+
return `${String(assignment.priority ?? 999999).padStart(6, "0")}#${assignment.createdAt ?? ""}#${assignment.id}`;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function targetLabelFromRelation(relation: any): string {
|
|
1205
|
+
const metadata = parseJsonObject(relation.metadata);
|
|
1206
|
+
return normalizeOptionalString(metadata?.title)
|
|
1207
|
+
?? normalizeOptionalString(metadata?.displayName)
|
|
1208
|
+
?? normalizeOptionalString(metadata?.externalItemId)
|
|
1209
|
+
?? relation.objectLineageId;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function parseJsonObject(value: unknown): Record<string, unknown> | null {
|
|
1213
|
+
if (!value) return null;
|
|
1214
|
+
if (typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
1215
|
+
if (typeof value !== "string") return null;
|
|
1216
|
+
try {
|
|
1217
|
+
const parsed = JSON.parse(value);
|
|
1218
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
|
|
1219
|
+
} catch {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function safeId(value: unknown): string {
|
|
1225
|
+
return String(value ?? "")
|
|
1226
|
+
.trim()
|
|
1227
|
+
.toLowerCase()
|
|
1228
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1229
|
+
.replace(/^-+|-+$/g, "")
|
|
1230
|
+
|| "item";
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function normalizeSummaryPayload(value: unknown, now: string): {
|
|
1234
|
+
generatedAt: string;
|
|
1235
|
+
staleAt: string;
|
|
1236
|
+
source: string;
|
|
1237
|
+
latestImportRun: Record<string, unknown> | null;
|
|
1238
|
+
counts: Record<string, number>;
|
|
1239
|
+
assignmentStatusCounts: Record<string, number>;
|
|
1240
|
+
assignmentTypeCounts: Record<string, number>;
|
|
1241
|
+
referenceStatusCounts: Record<string, number>;
|
|
1242
|
+
messageKindCounts: Record<string, number>;
|
|
1243
|
+
messageDomainCounts: Record<string, number>;
|
|
1244
|
+
facets: {
|
|
1245
|
+
assignments: {
|
|
1246
|
+
byStatus: Record<string, number>;
|
|
1247
|
+
byType: Record<string, number>;
|
|
1248
|
+
bySection: Record<string, number>;
|
|
1249
|
+
statusByType: Record<string, Record<string, number>>;
|
|
1250
|
+
statusBySection: Record<string, Record<string, number>>;
|
|
1251
|
+
typeBySection: Record<string, Record<string, number>>;
|
|
1252
|
+
};
|
|
1253
|
+
messages: { byKind: Record<string, number>; byDomain: Record<string, number>; byStatus: Record<string, number>; domainByKind: Record<string, Record<string, number>> };
|
|
1254
|
+
modelAttachments: { byOwnerKind: Record<string, number>; byRole: Record<string, number>; byMediaType: Record<string, number>; byStatus: Record<string, number> };
|
|
1255
|
+
references: { byCurationStatus: Record<string, number>; byCorpus: Record<string, number>; statusByCorpus: Record<string, Record<string, number>> };
|
|
1256
|
+
semanticNodes: { byNodeKind: Record<string, number>; byStatus: Record<string, number>; byCorpus: Record<string, number>; byCategorySet: Record<string, number> };
|
|
1257
|
+
semanticRelations: { byRelationTypeKey: Record<string, number>; byRelationDomain: Record<string, number>; bySubjectKind: Record<string, number>; byObjectKind: Record<string, number> };
|
|
1258
|
+
imports: { byCorpus: Record<string, number> };
|
|
1259
|
+
};
|
|
1260
|
+
} {
|
|
1261
|
+
const parsed = parseJsonObject(value) ?? {};
|
|
1262
|
+
const facets = normalizeFacets(parsed);
|
|
1263
|
+
return {
|
|
1264
|
+
generatedAt: normalizeOptionalString(parsed.generatedAt) ?? now,
|
|
1265
|
+
staleAt: normalizeOptionalString(parsed.staleAt) ?? now,
|
|
1266
|
+
source: normalizeOptionalString(parsed.source) ?? "missing",
|
|
1267
|
+
latestImportRun: parseJsonObject(parsed.latestImportRun),
|
|
1268
|
+
counts: numberRecord(parsed.counts),
|
|
1269
|
+
assignmentStatusCounts: numberRecord(parsed.assignmentStatusCounts),
|
|
1270
|
+
assignmentTypeCounts: numberRecord(parsed.assignmentTypeCounts),
|
|
1271
|
+
referenceStatusCounts: numberRecord(parsed.referenceStatusCounts),
|
|
1272
|
+
messageKindCounts: numberRecord(parsed.messageKindCounts),
|
|
1273
|
+
messageDomainCounts: numberRecord(parsed.messageDomainCounts),
|
|
1274
|
+
facets,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function normalizeFacets(payload: Record<string, unknown>): ReturnType<typeof createEmptyFacets> {
|
|
1279
|
+
const facets = createEmptyFacets();
|
|
1280
|
+
const parsed = parseJsonObject(payload.facets) ?? {};
|
|
1281
|
+
const assignments = parseJsonObject(parsed.assignments) ?? {};
|
|
1282
|
+
const messages = parseJsonObject(parsed.messages) ?? {};
|
|
1283
|
+
const modelAttachments = parseJsonObject(parsed.modelAttachments) ?? {};
|
|
1284
|
+
const references = parseJsonObject(parsed.references) ?? {};
|
|
1285
|
+
const semanticNodes = parseJsonObject(parsed.semanticNodes) ?? {};
|
|
1286
|
+
const semanticRelations = parseJsonObject(parsed.semanticRelations) ?? {};
|
|
1287
|
+
const imports = parseJsonObject(parsed.imports) ?? {};
|
|
1288
|
+
facets.assignments.byStatus = { ...numberRecord(payload.assignmentStatusCounts), ...numberRecord(assignments.byStatus) };
|
|
1289
|
+
facets.assignments.byType = { ...numberRecord(payload.assignmentTypeCounts), ...numberRecord(assignments.byType) };
|
|
1290
|
+
facets.assignments.bySection = numberRecord(assignments.bySection);
|
|
1291
|
+
facets.assignments.statusByType = nestedNumberRecord(assignments.statusByType);
|
|
1292
|
+
facets.assignments.statusBySection = nestedNumberRecord(assignments.statusBySection);
|
|
1293
|
+
facets.assignments.typeBySection = nestedNumberRecord(assignments.typeBySection);
|
|
1294
|
+
facets.messages.byKind = { ...numberRecord(payload.messageKindCounts), ...numberRecord(messages.byKind) };
|
|
1295
|
+
facets.messages.byDomain = { ...numberRecord(payload.messageDomainCounts), ...numberRecord(messages.byDomain) };
|
|
1296
|
+
facets.messages.byStatus = numberRecord(messages.byStatus);
|
|
1297
|
+
facets.messages.domainByKind = nestedNumberRecord(messages.domainByKind);
|
|
1298
|
+
facets.modelAttachments.byOwnerKind = numberRecord(modelAttachments.byOwnerKind);
|
|
1299
|
+
facets.modelAttachments.byRole = numberRecord(modelAttachments.byRole);
|
|
1300
|
+
facets.modelAttachments.byMediaType = numberRecord(modelAttachments.byMediaType);
|
|
1301
|
+
facets.modelAttachments.byStatus = numberRecord(modelAttachments.byStatus);
|
|
1302
|
+
facets.references.byCurationStatus = { ...numberRecord(payload.referenceStatusCounts), ...numberRecord(references.byCurationStatus) };
|
|
1303
|
+
facets.references.byCorpus = numberRecord(references.byCorpus);
|
|
1304
|
+
facets.references.statusByCorpus = nestedNumberRecord(references.statusByCorpus);
|
|
1305
|
+
facets.semanticNodes.byNodeKind = numberRecord(semanticNodes.byNodeKind);
|
|
1306
|
+
facets.semanticNodes.byStatus = numberRecord(semanticNodes.byStatus);
|
|
1307
|
+
facets.semanticNodes.byCorpus = numberRecord(semanticNodes.byCorpus);
|
|
1308
|
+
facets.semanticNodes.byCategorySet = numberRecord(semanticNodes.byCategorySet);
|
|
1309
|
+
facets.semanticRelations.byRelationTypeKey = numberRecord(semanticRelations.byRelationTypeKey);
|
|
1310
|
+
facets.semanticRelations.byRelationDomain = numberRecord(semanticRelations.byRelationDomain);
|
|
1311
|
+
facets.semanticRelations.bySubjectKind = numberRecord(semanticRelations.bySubjectKind);
|
|
1312
|
+
facets.semanticRelations.byObjectKind = numberRecord(semanticRelations.byObjectKind);
|
|
1313
|
+
facets.imports.byCorpus = numberRecord(imports.byCorpus);
|
|
1314
|
+
return facets;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function createEmptyFacets() {
|
|
1318
|
+
return {
|
|
1319
|
+
assignments: { byStatus: {}, byType: {}, bySection: {}, statusByType: {}, statusBySection: {}, typeBySection: {} },
|
|
1320
|
+
messages: { byKind: {}, byDomain: {}, byStatus: {}, domainByKind: {} },
|
|
1321
|
+
modelAttachments: { byOwnerKind: {}, byRole: {}, byMediaType: {}, byStatus: {} },
|
|
1322
|
+
references: { byCurationStatus: {}, byCorpus: {}, statusByCorpus: {} },
|
|
1323
|
+
semanticNodes: { byNodeKind: {}, byStatus: {}, byCorpus: {}, byCategorySet: {} },
|
|
1324
|
+
semanticRelations: { byRelationTypeKey: {}, byRelationDomain: {}, bySubjectKind: {}, byObjectKind: {} },
|
|
1325
|
+
imports: { byCorpus: {} },
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function nestedNumberRecord(value: unknown): Record<string, Record<string, number>> {
|
|
1330
|
+
const parsed = parseJsonObject(value) ?? {};
|
|
1331
|
+
const result: Record<string, Record<string, number>> = {};
|
|
1332
|
+
for (const [key, entry] of Object.entries(parsed)) result[key] = numberRecord(entry);
|
|
1333
|
+
return result;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function upsertNewsroomSummaryPayload(
|
|
1337
|
+
client: DataClient,
|
|
1338
|
+
payload: Record<string, unknown>,
|
|
1339
|
+
current: any,
|
|
1340
|
+
now: string,
|
|
1341
|
+
): Promise<void> {
|
|
1342
|
+
const input = {
|
|
1343
|
+
id: NEWSROOM_SUMMARY_PAYLOAD_ID,
|
|
1344
|
+
ownerType: "newsroom",
|
|
1345
|
+
ownerId: "newsroom",
|
|
1346
|
+
payloadKind: "summary-snapshot",
|
|
1347
|
+
importRunId: normalizeOptionalString(parseJsonObject(payload.latestImportRun)?.id),
|
|
1348
|
+
createdAt: current?.createdAt ?? now,
|
|
1349
|
+
updatedAt: now,
|
|
1350
|
+
};
|
|
1351
|
+
if (current) {
|
|
1352
|
+
await requireDataResult(client.models.KnowledgeRawPayload.update(input), "update Newsroom summary snapshot");
|
|
1353
|
+
} else {
|
|
1354
|
+
await requireDataResult(client.models.KnowledgeRawPayload.create(input), "create Newsroom summary snapshot");
|
|
1355
|
+
}
|
|
1356
|
+
await putJsonModelPayload(
|
|
1357
|
+
client as any,
|
|
1358
|
+
{ ownerKind: NEWSROOM_SUMMARY_PAYLOAD_OWNER_KIND, ownerId: NEWSROOM_SUMMARY_PAYLOAD_ID, importRunId: input.importRunId },
|
|
1359
|
+
"raw_payload",
|
|
1360
|
+
"summary-snapshot",
|
|
1361
|
+
payload,
|
|
1362
|
+
{ filename: "summary-snapshot.json", now },
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function readNewsroomSummaryPayload(client: DataClient): Promise<Record<string, unknown> | null> {
|
|
1367
|
+
return readJsonModelPayload(
|
|
1368
|
+
client as any,
|
|
1369
|
+
NEWSROOM_SUMMARY_PAYLOAD_OWNER_KIND,
|
|
1370
|
+
NEWSROOM_SUMMARY_PAYLOAD_ID,
|
|
1371
|
+
"raw_payload",
|
|
1372
|
+
"summary-snapshot",
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function numberRecord(value: unknown): Record<string, number> {
|
|
1377
|
+
const parsed = parseJsonObject(value) ?? {};
|
|
1378
|
+
const result: Record<string, number> = {};
|
|
1379
|
+
for (const [key, entry] of Object.entries(parsed)) {
|
|
1380
|
+
const numeric = Number(entry);
|
|
1381
|
+
if (Number.isFinite(numeric)) result[key] = numeric;
|
|
1382
|
+
}
|
|
1383
|
+
return result;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function increment(target: Record<string, number>, key: string, delta: number): void {
|
|
1387
|
+
target[key] = Math.max(0, (target[key] ?? 0) + delta);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function normalizeRequiredString(value: unknown, fieldName: string): string {
|
|
1391
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${fieldName} is required.`);
|
|
1392
|
+
return value.trim();
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function normalizeOptionalString(value: unknown): string | null {
|
|
1396
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function getIdentitySub(event: any): string | null {
|
|
1400
|
+
const identity = event.identity as { sub?: string | null; claims?: Record<string, unknown> } | null | undefined;
|
|
1401
|
+
return normalizeOptionalString(identity?.sub) ?? normalizeOptionalString(identity?.claims?.sub);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function getIdentityLabel(event: any): string | null {
|
|
1405
|
+
const identity = event.identity as { username?: string | null; claims?: Record<string, unknown> } | null | undefined;
|
|
1406
|
+
return normalizeOptionalString(identity?.claims?.email)
|
|
1407
|
+
?? normalizeOptionalString(identity?.claims?.name)
|
|
1408
|
+
?? normalizeOptionalString(identity?.username)
|
|
1409
|
+
?? getIdentitySub(event);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function toAwsJson(value: unknown): string | null {
|
|
1413
|
+
if (value === null || value === undefined) return null;
|
|
1414
|
+
if (typeof value === "string") return value;
|
|
1415
|
+
return JSON.stringify(value);
|
|
1416
|
+
}
|