@anthusai/papyrus 1.0.0-next.61 → 1.0.0-next.63
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 +4 -0
- package/amplify/data/resource.ts +4 -0
- package/amplify/data/schema.js +4 -0
- package/amplify/data/schema.ts +4 -0
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/cyclotron/page.tsx +13 -0
- package/components/news-desk-taxonomy-client.ts +37 -1
- package/components/newsroom-board-view.tsx +7 -8
- package/components/priming-proof-page.tsx +94 -0
- package/components/topic-steering-workspace.tsx +4 -0
- package/infra/amplify-app-shell.js +18 -1
- package/infra/amplify-app-shell.ts +21 -1
- package/infra/site-config.js +21 -3
- package/infra/site-config.ts +29 -3
- package/lib/approved-priming-proof-reports.ts +6 -0
- package/lib/category-repository.ts +1 -0
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/newsroom-board.ts +60 -21
- package/lib/newsroom-nav.ts +2 -1
- package/lib/newsroom-organizer-nav.ts +5 -2
- package/lib/priming-proof-report.ts +90 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/site-brand.ts +19 -0
- package/package.json +13 -1
- package/routes.manifest.json +17 -1
package/amplify/data/resource.js
CHANGED
|
@@ -893,9 +893,11 @@ const schema = a.schema({
|
|
|
893
893
|
createdAt: a.datetime().required(),
|
|
894
894
|
updatedAt: a.datetime().required(),
|
|
895
895
|
newsroomFeedKey: a.string(),
|
|
896
|
+
itemLineageId: a.id(),
|
|
896
897
|
})
|
|
897
898
|
.secondaryIndexes((index) => [
|
|
898
899
|
index("newsroomFeedKey").sortKeys(["createdAt"]).queryField("listAssignmentsByNewsroomFeedAndCreatedAt"),
|
|
900
|
+
index("itemLineageId").sortKeys(["createdAt"]).queryField("listAssignmentsByItemLineageAndCreatedAt"),
|
|
899
901
|
index("queueStatusKey").sortKeys(["priority", "createdAt"]).queryField("listAssignmentsByQueueStatusAndPriority"),
|
|
900
902
|
index("assignmentTypeKey").sortKeys(["status", "createdAt"]).queryField("listAssignmentsByTypeStatusAndCreatedAt"),
|
|
901
903
|
index("status").sortKeys(["updatedAt"]).queryField("listAssignmentsByStatusAndUpdatedAt"),
|
|
@@ -1632,6 +1634,7 @@ const schema = a.schema({
|
|
|
1632
1634
|
type: a.string().required(),
|
|
1633
1635
|
status: a.string().required(),
|
|
1634
1636
|
typeStatus: a.string().required(),
|
|
1637
|
+
stage: a.string(),
|
|
1635
1638
|
slug: a.string().required(),
|
|
1636
1639
|
shortSlug: a.string(),
|
|
1637
1640
|
section: a.string(),
|
|
@@ -1663,6 +1666,7 @@ const schema = a.schema({
|
|
|
1663
1666
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1664
1667
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1665
1668
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
1669
|
+
index("stage").sortKeys(["updatedAt"]).queryField("listItemsByStageAndUpdatedAt"),
|
|
1666
1670
|
])
|
|
1667
1671
|
.authorization((allow) => [
|
|
1668
1672
|
allow.groups(contentWriteGroups),
|
package/amplify/data/resource.ts
CHANGED
|
@@ -970,9 +970,11 @@ const schema = a.schema({
|
|
|
970
970
|
createdAt: a.datetime().required(),
|
|
971
971
|
updatedAt: a.datetime().required(),
|
|
972
972
|
newsroomFeedKey: a.string(),
|
|
973
|
+
itemLineageId: a.id(),
|
|
973
974
|
})
|
|
974
975
|
.secondaryIndexes((index) => [
|
|
975
976
|
index("newsroomFeedKey").sortKeys(["createdAt"]).queryField("listAssignmentsByNewsroomFeedAndCreatedAt"),
|
|
977
|
+
index("itemLineageId").sortKeys(["createdAt"]).queryField("listAssignmentsByItemLineageAndCreatedAt"),
|
|
976
978
|
index("queueStatusKey").sortKeys(["priority", "createdAt"]).queryField("listAssignmentsByQueueStatusAndPriority"),
|
|
977
979
|
index("assignmentTypeKey").sortKeys(["status", "createdAt"]).queryField("listAssignmentsByTypeStatusAndCreatedAt"),
|
|
978
980
|
index("status").sortKeys(["updatedAt"]).queryField("listAssignmentsByStatusAndUpdatedAt"),
|
|
@@ -1733,6 +1735,7 @@ const schema = a.schema({
|
|
|
1733
1735
|
type: a.string().required(),
|
|
1734
1736
|
status: a.string().required(),
|
|
1735
1737
|
typeStatus: a.string().required(),
|
|
1738
|
+
stage: a.string(),
|
|
1736
1739
|
slug: a.string().required(),
|
|
1737
1740
|
shortSlug: a.string(),
|
|
1738
1741
|
section: a.string(),
|
|
@@ -1764,6 +1767,7 @@ const schema = a.schema({
|
|
|
1764
1767
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1765
1768
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1766
1769
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
1770
|
+
index("stage").sortKeys(["updatedAt"]).queryField("listItemsByStageAndUpdatedAt"),
|
|
1767
1771
|
])
|
|
1768
1772
|
.authorization((allow) => [
|
|
1769
1773
|
allow.groups(contentWriteGroups),
|
package/amplify/data/schema.js
CHANGED
|
@@ -799,9 +799,11 @@ const baseDefinitions = {
|
|
|
799
799
|
createdAt: a.datetime().required(),
|
|
800
800
|
updatedAt: a.datetime().required(),
|
|
801
801
|
newsroomFeedKey: a.string(),
|
|
802
|
+
itemLineageId: a.id(),
|
|
802
803
|
})
|
|
803
804
|
.secondaryIndexes((index) => [
|
|
804
805
|
index("newsroomFeedKey").sortKeys(["createdAt"]).queryField("listAssignmentsByNewsroomFeedAndCreatedAt"),
|
|
806
|
+
index("itemLineageId").sortKeys(["createdAt"]).queryField("listAssignmentsByItemLineageAndCreatedAt"),
|
|
805
807
|
index("queueStatusKey").sortKeys(["priority", "createdAt"]).queryField("listAssignmentsByQueueStatusAndPriority"),
|
|
806
808
|
index("assignmentTypeKey").sortKeys(["status", "createdAt"]).queryField("listAssignmentsByTypeStatusAndCreatedAt"),
|
|
807
809
|
index("status").sortKeys(["updatedAt"]).queryField("listAssignmentsByStatusAndUpdatedAt"),
|
|
@@ -1511,6 +1513,7 @@ const baseDefinitions = {
|
|
|
1511
1513
|
changeReason: a.string(),
|
|
1512
1514
|
contentHash: a.string(),
|
|
1513
1515
|
type: a.string().required(),
|
|
1516
|
+
stage: a.string(),
|
|
1514
1517
|
status: a.string().required(),
|
|
1515
1518
|
typeStatus: a.string().required(),
|
|
1516
1519
|
slug: a.string().required(),
|
|
@@ -1544,6 +1547,7 @@ const baseDefinitions = {
|
|
|
1544
1547
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1545
1548
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1546
1549
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
1550
|
+
index("stage").sortKeys(["updatedAt"]).queryField("listItemsByStageAndUpdatedAt"),
|
|
1547
1551
|
])
|
|
1548
1552
|
.authorization((allow) => [
|
|
1549
1553
|
allow.groups(contentWriteGroups),
|
package/amplify/data/schema.ts
CHANGED
|
@@ -870,9 +870,11 @@ const baseDefinitions = {
|
|
|
870
870
|
createdAt: a.datetime().required(),
|
|
871
871
|
updatedAt: a.datetime().required(),
|
|
872
872
|
newsroomFeedKey: a.string(),
|
|
873
|
+
itemLineageId: a.id(),
|
|
873
874
|
})
|
|
874
875
|
.secondaryIndexes((index) => [
|
|
875
876
|
index("newsroomFeedKey").sortKeys(["createdAt"]).queryField("listAssignmentsByNewsroomFeedAndCreatedAt"),
|
|
877
|
+
index("itemLineageId").sortKeys(["createdAt"]).queryField("listAssignmentsByItemLineageAndCreatedAt"),
|
|
876
878
|
index("queueStatusKey").sortKeys(["priority", "createdAt"]).queryField("listAssignmentsByQueueStatusAndPriority"),
|
|
877
879
|
index("assignmentTypeKey").sortKeys(["status", "createdAt"]).queryField("listAssignmentsByTypeStatusAndCreatedAt"),
|
|
878
880
|
index("status").sortKeys(["updatedAt"]).queryField("listAssignmentsByStatusAndUpdatedAt"),
|
|
@@ -1606,6 +1608,7 @@ const baseDefinitions = {
|
|
|
1606
1608
|
changeReason: a.string(),
|
|
1607
1609
|
contentHash: a.string(),
|
|
1608
1610
|
type: a.string().required(),
|
|
1611
|
+
stage: a.string(),
|
|
1609
1612
|
status: a.string().required(),
|
|
1610
1613
|
typeStatus: a.string().required(),
|
|
1611
1614
|
slug: a.string().required(),
|
|
@@ -1639,6 +1642,7 @@ const baseDefinitions = {
|
|
|
1639
1642
|
index("typeStatus").sortKeys(["publishedAt"]).queryField("listItemsByTypeStatusAndPublishedAt"),
|
|
1640
1643
|
index("sectionStatus").sortKeys(["publishedAt"]).queryField("listItemsBySectionStatusAndPublishedAt"),
|
|
1641
1644
|
index("versionState").sortKeys(["updatedAt"]).queryField("listItemsByVersionStateAndUpdatedAt"),
|
|
1645
|
+
index("stage").sortKeys(["updatedAt"]).queryField("listItemsByStageAndUpdatedAt"),
|
|
1642
1646
|
])
|
|
1643
1647
|
.authorization((allow) => [
|
|
1644
1648
|
allow.groups(contentWriteGroups),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { approvedPrimingProofReport } from "../../../../lib/priming-proof-report";
|
|
3
|
+
import { SITE_BRAND } from "../../../../lib/site-brand";
|
|
4
|
+
|
|
5
|
+
export function GET() {
|
|
6
|
+
const report = approvedPrimingProofReport(SITE_BRAND.id);
|
|
7
|
+
if (report === null) {
|
|
8
|
+
return NextResponse.json({ error: "No approved priming proof report for this publication yet." }, { status: 404 });
|
|
9
|
+
}
|
|
10
|
+
return NextResponse.json(report);
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { PrimingProofPage } from "../../components/priming-proof-page";
|
|
3
|
+
import { approvedPrimingProofReport } from "../../lib/priming-proof-report";
|
|
4
|
+
import { SITE_BRAND } from "../../lib/site-brand";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: `Cyclotron proof | ${SITE_BRAND.articleTitleSuffix}`,
|
|
8
|
+
description: "How often the relevance classifier agrees with editors on a fixed held-out set, before and after editor labels.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function CyclotronProof() {
|
|
12
|
+
return <PrimingProofPage report={approvedPrimingProofReport(SITE_BRAND.id)} />;
|
|
13
|
+
}
|
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
UserDirectoryEntry,
|
|
40
40
|
} from "../lib/category-repository";
|
|
41
41
|
import { relationTypeKey } from "../lib/semantic-graph";
|
|
42
|
+
import { CONTENT_ITEM_STAGES, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
42
43
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
43
44
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
44
45
|
import { isUnauthenticatedError, loadReaderSessionSnapshot, type ReaderAuthSnapshot } from "./reader-auth-state";
|
|
@@ -253,7 +254,7 @@ const UPDATE_MESSAGE_MUTATION = `
|
|
|
253
254
|
const NEWSROOM_ASSIGNMENT_FEED_QUERY = `
|
|
254
255
|
query ListAssignmentsByNewsroomFeedAndCreatedAt($newsroomFeedKey: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelAssignmentFilterInput) {
|
|
255
256
|
listAssignmentsByNewsroomFeedAndCreatedAt(newsroomFeedKey: $newsroomFeedKey, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
|
|
256
|
-
items { id assignmentTypeKey queueKey queueStatusKey status priority title summary assigneeType assigneeId assigneeKey claimedAt claimExpiresAt completedAt canceledAt corpusId categorySetId classifierId sectionId sectionKey sectionType sectionStatusKey sectionQueueStatusKey primaryFocusCategoryKey topicScopeCategoryKeys sourceSnapshotId importRunId createdBy createdAt updatedAt newsroomFeedKey }
|
|
257
|
+
items { id assignmentTypeKey queueKey queueStatusKey status priority title summary assigneeType assigneeId assigneeKey claimedAt claimExpiresAt completedAt canceledAt corpusId categorySetId classifierId sectionId sectionKey sectionType sectionStatusKey sectionQueueStatusKey primaryFocusCategoryKey topicScopeCategoryKeys sourceSnapshotId importRunId createdBy createdAt updatedAt newsroomFeedKey itemLineageId }
|
|
257
258
|
nextToken
|
|
258
259
|
}
|
|
259
260
|
}
|
|
@@ -2157,6 +2158,41 @@ export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPage
|
|
|
2157
2158
|
});
|
|
2158
2159
|
}
|
|
2159
2160
|
|
|
2161
|
+
const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
2162
|
+
query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
2163
|
+
listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
2164
|
+
items { id lineageId versionNumber stage type status title headline slug section updatedAt }
|
|
2165
|
+
nextToken
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
`;
|
|
2169
|
+
|
|
2170
|
+
export async function loadNewsroomContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2171
|
+
configureAmplifyClient();
|
|
2172
|
+
const client = generateClient<Schema>() as unknown as {
|
|
2173
|
+
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
|
|
2174
|
+
};
|
|
2175
|
+
const field = "listItemsByStageAndUpdatedAt";
|
|
2176
|
+
const versionsPerStage = await Promise.all(CONTENT_ITEM_STAGES.map(async (stage) => {
|
|
2177
|
+
const versions: ContentItemVersionRecord[] = [];
|
|
2178
|
+
let nextToken: string | null = null;
|
|
2179
|
+
do {
|
|
2180
|
+
const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
|
|
2181
|
+
query: CONTENT_ITEMS_BY_STAGE_QUERY,
|
|
2182
|
+
variables: { stage, sortDirection: "DESC", limit: NEWSROOM_PAGE_LIMIT, nextToken },
|
|
2183
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
2184
|
+
});
|
|
2185
|
+
assertNoGraphQLErrors(response.errors);
|
|
2186
|
+
const connection = response.data?.[field];
|
|
2187
|
+
if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
|
|
2188
|
+
versions.push(...((connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[]));
|
|
2189
|
+
nextToken = connection.nextToken ?? null;
|
|
2190
|
+
} while (nextToken);
|
|
2191
|
+
return versions;
|
|
2192
|
+
}));
|
|
2193
|
+
return versionsPerStage.flat();
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2160
2196
|
export async function loadModelPayloadsForOwner(
|
|
2161
2197
|
ownerKind: string,
|
|
2162
2198
|
ownerId: string,
|
|
@@ -6,10 +6,9 @@ import { IssueCollection, IssueDetailPanel, IssueTypeFilter, issueTypeFromFilter
|
|
|
6
6
|
import "@kanbus/ui/styles/kanban.css";
|
|
7
7
|
import "./newsroom-newsprint-board.css";
|
|
8
8
|
import { canReadNewsroomBoard, newsroomBoardConfig, projectNewsroomBoard, type NativeBoardIssue } from "../lib/newsroom-board";
|
|
9
|
-
import {
|
|
10
|
-
import { SITE_BRAND } from "../lib/site-brand";
|
|
9
|
+
import { getContentItemName, SITE_BRAND } from "../lib/site-brand";
|
|
11
10
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
12
|
-
import { loadNewsroomAssignmentPage, loadNewsroomReferencePage } from "./news-desk-taxonomy-client";
|
|
11
|
+
import { loadNewsroomAssignmentPage, loadNewsroomContentItemVersions, loadNewsroomReferencePage } from "./news-desk-taxonomy-client";
|
|
13
12
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
14
13
|
import { NewsDeskAccessGate } from "./newsroom-access-gate";
|
|
15
14
|
import { NewsroomOpsShell, NewsroomOpsStatusBanner } from "./newsroom-ops-shell";
|
|
@@ -45,12 +44,12 @@ export function NewsroomBoardView() {
|
|
|
45
44
|
if (!allowed) { setLoading(false); return; }
|
|
46
45
|
setLoading(true); busy.current = true;
|
|
47
46
|
Promise.all([
|
|
48
|
-
|
|
47
|
+
loadNewsroomContentItemVersions(),
|
|
49
48
|
loadNewsroomReferencePage({ limit: 50 }),
|
|
50
49
|
loadNewsroomAssignmentPage({ limit: 50 }),
|
|
51
|
-
]).then(([
|
|
50
|
+
]).then(([contentItemVersions, references, assignments]) => {
|
|
52
51
|
if (generation.current !== request) return;
|
|
53
|
-
setIssues(projectNewsroomBoard({
|
|
52
|
+
setIssues(projectNewsroomBoard({ contentItemVersions, references: references.items, assignments: assignments.items }));
|
|
54
53
|
setTokens({ reference: references.nextToken, assignment: assignments.nextToken });
|
|
55
54
|
}).catch((reason: unknown) => {
|
|
56
55
|
if (generation.current === request) setError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
|
|
@@ -81,7 +80,7 @@ export function NewsroomBoardView() {
|
|
|
81
80
|
<IssueTypeFilter config={config} value={filter} onChange={value => { setFilter(value); setSelected(null); }} name="Record type" />
|
|
82
81
|
<div role="group" aria-label="Record presentation"><button type="button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button><button type="button" disabled={loading} onClick={() => setRevision(current => current + 1)}>Refresh records</button></div>
|
|
83
82
|
</div>
|
|
84
|
-
<p className="organizer-board-note">
|
|
83
|
+
<p className="organizer-board-note">{`Each ${getContentItemName().singular.toLowerCase()} appears once, in the column for its stage. Open a record to use its existing editor or workflow controls.`}</p>
|
|
85
84
|
{error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
|
|
86
85
|
<p role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded. References and assignments load in pages.`}</p>
|
|
87
86
|
<div className="organizer-board-body" data-detail-open={Boolean(issue)}>
|
|
@@ -91,7 +90,7 @@ export function NewsroomBoardView() {
|
|
|
91
90
|
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel="Newsroom records"
|
|
92
91
|
emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
|
|
93
92
|
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[{ label: "CMS record ID", value: issue.nativeId }, { label: "Status", value: issue.nativeStatus }]}>
|
|
94
|
-
<Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}
|
|
93
|
+
<Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}</Link>
|
|
95
94
|
</IssueDetailPanel>}
|
|
96
95
|
</div>
|
|
97
96
|
<div className="organizer-board-pages">{tokens.reference && <button disabled={loading} type="button" onClick={() => void loadMore("reference")}>Load more references</button>}{tokens.assignment && <button disabled={loading} type="button" onClick={() => void loadMore("assignment")}>Load more assignments</button>}</div>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatProofPercentage,
|
|
3
|
+
primingProofHeadline,
|
|
4
|
+
type PrimingProofReport,
|
|
5
|
+
} from "../lib/priming-proof-report";
|
|
6
|
+
|
|
7
|
+
function ProofMethod({ heldOutSize, teachingSource }: { heldOutSize: number | null; teachingSource: string | null }) {
|
|
8
|
+
return (
|
|
9
|
+
<section aria-labelledby="priming-proof-method" className="space-y-2 text-sm leading-relaxed">
|
|
10
|
+
<h2 id="priming-proof-method" className="text-base font-semibold uppercase tracking-wide">How the number is made</h2>
|
|
11
|
+
<p>
|
|
12
|
+
Editors already decided which candidate sources belong in this publication. A fixed, balanced sample of those
|
|
13
|
+
decisions{heldOutSize === null ? "" : ` (${heldOutSize} sources)`} is frozen as the held-out set and never used
|
|
14
|
+
for teaching. A classifier that always gives the same answer agrees on half of it.
|
|
15
|
+
</p>
|
|
16
|
+
<p>
|
|
17
|
+
Editors teach the classifier with labels: their other reviews of candidate sources, and sources they paste,
|
|
18
|
+
check after Papyrus extracts them, and mark publish or reject.
|
|
19
|
+
{teachingSource === null ? "" : ` This run taught from ${teachingSource}.`} Each point below starts a fresh
|
|
20
|
+
classifier that sees only the first N of those labels, then decides
|
|
21
|
+
every held-out source once. Nothing else changes between points: the same decision model, the publication's
|
|
22
|
+
mission and policies as the only rubric, and no automatic rubric rewriting.
|
|
23
|
+
</p>
|
|
24
|
+
</section>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function PrimingProofPage({ report }: { report: PrimingProofReport | null }) {
|
|
29
|
+
if (report === null) {
|
|
30
|
+
return (
|
|
31
|
+
<main className="mx-auto max-w-3xl space-y-6 px-4 py-10" data-priming-proof="pending">
|
|
32
|
+
<header className="space-y-2 border-b pb-4">
|
|
33
|
+
<h1 className="text-3xl font-bold">Does teaching the classifier work?</h1>
|
|
34
|
+
<p className="text-muted-foreground">No approved proof run has been published for this publication yet.</p>
|
|
35
|
+
</header>
|
|
36
|
+
<ProofMethod heldOutSize={null} teachingSource={null} />
|
|
37
|
+
</main>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const { baseline, latest, agreementChangeInPoints } = primingProofHeadline(report);
|
|
41
|
+
const [positiveLabel, negativeLabel] = [report.positiveLabel, report.labels.find((label) => label !== report.positiveLabel)];
|
|
42
|
+
return (
|
|
43
|
+
<main className="mx-auto max-w-3xl space-y-8 px-4 py-10" data-priming-proof="published">
|
|
44
|
+
<header className="space-y-3 border-b pb-4">
|
|
45
|
+
<h1 className="text-3xl font-bold">Does teaching the classifier work?</h1>
|
|
46
|
+
<p className="text-lg" data-priming-proof-headline>
|
|
47
|
+
Agreement with editors went from <strong>{formatProofPercentage(baseline.agreement)}</strong> with no labels to{" "}
|
|
48
|
+
<strong>{formatProofPercentage(latest.agreement)}</strong> after {latest.labels} labels
|
|
49
|
+
{agreementChangeInPoints === null ? "" : ` (${agreementChangeInPoints >= 0 ? "+" : ""}${agreementChangeInPoints} points)`}.
|
|
50
|
+
</p>
|
|
51
|
+
<p className="text-sm text-muted-foreground">
|
|
52
|
+
{report.publication} · question: “{report.question}” · held-out set of {report.heldOut.size} sources frozen{" "}
|
|
53
|
+
{report.heldOut.frozenAt.slice(0, 10)} · run {report.ranAt.slice(0, 10)}
|
|
54
|
+
{report.decisionModel ? ` · decision model ${report.decisionModel}` : ""}
|
|
55
|
+
</p>
|
|
56
|
+
</header>
|
|
57
|
+
<section aria-labelledby="priming-proof-points" className="space-y-2">
|
|
58
|
+
<h2 id="priming-proof-points" className="text-base font-semibold uppercase tracking-wide">Before and after</h2>
|
|
59
|
+
<div className="overflow-x-auto">
|
|
60
|
+
<table className="w-full text-left text-sm tabular-nums">
|
|
61
|
+
<thead>
|
|
62
|
+
<tr className="border-b">
|
|
63
|
+
<th scope="col" className="py-2 pr-4">Editor labels</th>
|
|
64
|
+
<th scope="col" className="py-2 pr-4">Agreement</th>
|
|
65
|
+
<th scope="col" className="py-2 pr-4">Precision ({positiveLabel})</th>
|
|
66
|
+
<th scope="col" className="py-2 pr-4">Recall ({positiveLabel})</th>
|
|
67
|
+
<th scope="col" className="py-2">Labels used</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
{report.points.map((point) => (
|
|
72
|
+
<tr key={point.labels} className="border-b" data-priming-proof-point={point.labels}>
|
|
73
|
+
<td className="py-2 pr-4">{point.labels}</td>
|
|
74
|
+
<td className="py-2 pr-4">{formatProofPercentage(point.agreement)}</td>
|
|
75
|
+
<td className="py-2 pr-4">{formatProofPercentage(point.precision)}</td>
|
|
76
|
+
<td className="py-2 pr-4">{formatProofPercentage(point.recall)}</td>
|
|
77
|
+
<td className="py-2">
|
|
78
|
+
{point.labelsByClass[positiveLabel] ?? 0} {positiveLabel}
|
|
79
|
+
{negativeLabel ? `, ${point.labelsByClass[negativeLabel] ?? 0} ${negativeLabel}` : ""}
|
|
80
|
+
</td>
|
|
81
|
+
</tr>
|
|
82
|
+
))}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
<ProofMethod heldOutSize={report.heldOut.size} teachingSource={report.priming.source} />
|
|
88
|
+
<p className="break-all text-xs text-muted-foreground">
|
|
89
|
+
Held-out fingerprint {report.heldOut.fingerprint}
|
|
90
|
+
{report.priming.fingerprint ? ` · labels fingerprint ${report.priming.fingerprint}` : ""}
|
|
91
|
+
</p>
|
|
92
|
+
</main>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -15042,6 +15042,9 @@ function buildUiCopywritingAssignment({ actorLabel, assignment, decision, messag
|
|
|
15042
15042
|
message: MessageRecord;
|
|
15043
15043
|
now: string;
|
|
15044
15044
|
}): AssignmentRecord {
|
|
15045
|
+
if (!assignment.itemLineageId) {
|
|
15046
|
+
throw new Error(`Assignment ${assignment.id} has no content item lineage; copywriting must continue the same content item.`);
|
|
15047
|
+
}
|
|
15045
15048
|
const type = decision === "brief" ? "brief" : "article";
|
|
15046
15049
|
const assignmentTypeKey = type === "brief" ? "copywriting.brief-draft" : "copywriting.article-draft";
|
|
15047
15050
|
const section = assignment.sectionKey ?? assignment.sectionId ?? "unsectioned";
|
|
@@ -15094,6 +15097,7 @@ function buildUiCopywritingAssignment({ actorLabel, assignment, decision, messag
|
|
|
15094
15097
|
createdAt: now,
|
|
15095
15098
|
updatedAt: now,
|
|
15096
15099
|
newsroomFeedKey: "assignment#open",
|
|
15100
|
+
itemLineageId: assignment.itemLineageId,
|
|
15097
15101
|
metadata,
|
|
15098
15102
|
};
|
|
15099
15103
|
}
|
|
@@ -3,7 +3,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
3
3
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
4
4
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
5
5
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
6
|
-
import { hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
6
|
+
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
7
|
function environmentVariables(variables) {
|
|
8
8
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
9
9
|
}
|
|
@@ -85,6 +85,17 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
85
85
|
],
|
|
86
86
|
}));
|
|
87
87
|
}
|
|
88
|
+
const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
|
|
89
|
+
const prospectusEnvironment = prospectusAccountsApiStageArn
|
|
90
|
+
? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
|
|
91
|
+
: {};
|
|
92
|
+
if (prospectusAccountsApiStageArn) {
|
|
93
|
+
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
94
|
+
sid: "InvokeProspectusAccountsAsRegisteredSite",
|
|
95
|
+
actions: ["execute-api:Invoke"],
|
|
96
|
+
resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
88
99
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
89
100
|
name: cmsAppName,
|
|
90
101
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
@@ -134,6 +145,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
134
145
|
SITE_ENV: "production",
|
|
135
146
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
136
147
|
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
148
|
+
...prospectusEnvironment,
|
|
137
149
|
}),
|
|
138
150
|
});
|
|
139
151
|
let stagingBranch;
|
|
@@ -153,6 +165,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
153
165
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
154
166
|
SITE_ENV: "staging",
|
|
155
167
|
...stagingMode,
|
|
168
|
+
...prospectusEnvironment,
|
|
156
169
|
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
157
170
|
? `https://${stagingDomainName}`
|
|
158
171
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
@@ -191,6 +204,10 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
191
204
|
this.cmsAppId = cmsApp.attrAppId;
|
|
192
205
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
193
206
|
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
207
|
+
new CfnOutput(this, "CmsComputeRoleArn", {
|
|
208
|
+
value: computeRole.roleArn,
|
|
209
|
+
description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
|
|
210
|
+
});
|
|
194
211
|
if (stagingEnabled) {
|
|
195
212
|
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
196
213
|
}
|
|
@@ -4,7 +4,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
4
4
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
5
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
7
|
-
import { AmplifyAppShellSiteConfig, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
7
|
+
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
8
|
|
|
9
9
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
10
10
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
@@ -96,6 +96,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
|
|
100
|
+
const prospectusEnvironment = prospectusAccountsApiStageArn
|
|
101
|
+
? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
|
|
102
|
+
: {};
|
|
103
|
+
if (prospectusAccountsApiStageArn) {
|
|
104
|
+
computeRole.addToPolicy(
|
|
105
|
+
new iam.PolicyStatement({
|
|
106
|
+
sid: "InvokeProspectusAccountsAsRegisteredSite",
|
|
107
|
+
actions: ["execute-api:Invoke"],
|
|
108
|
+
resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
100
114
|
name: cmsAppName,
|
|
101
115
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
@@ -148,6 +162,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
148
162
|
SITE_ENV: "production",
|
|
149
163
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
150
164
|
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
165
|
+
...prospectusEnvironment,
|
|
151
166
|
}),
|
|
152
167
|
});
|
|
153
168
|
|
|
@@ -168,6 +183,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
168
183
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
169
184
|
SITE_ENV: "staging",
|
|
170
185
|
...stagingMode,
|
|
186
|
+
...prospectusEnvironment,
|
|
171
187
|
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
172
188
|
? `https://${stagingDomainName}`
|
|
173
189
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
@@ -209,6 +225,10 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
209
225
|
this.cmsAppId = cmsApp.attrAppId;
|
|
210
226
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
211
227
|
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
228
|
+
new CfnOutput(this, "CmsComputeRoleArn", {
|
|
229
|
+
value: computeRole.roleArn,
|
|
230
|
+
description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
|
|
231
|
+
});
|
|
212
232
|
if (stagingEnabled) {
|
|
213
233
|
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
214
234
|
}
|
package/infra/site-config.js
CHANGED
|
@@ -16,7 +16,24 @@ const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}
|
|
|
16
16
|
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
17
17
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
18
18
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
19
|
-
const
|
|
19
|
+
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
20
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT"];
|
|
21
|
+
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
22
|
+
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
23
|
+
"GET/v1/identities/me",
|
|
24
|
+
"POST/v1/identities/google/challenge",
|
|
25
|
+
"POST/v1/identities/google/resolve",
|
|
26
|
+
"GET/v1/accounts",
|
|
27
|
+
"GET/v1/accounts/*",
|
|
28
|
+
"POST/v1/accounts/*/authorize",
|
|
29
|
+
"GET/v1/account-directory",
|
|
30
|
+
"GET/v1/account-directory/*",
|
|
31
|
+
];
|
|
32
|
+
export function resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) {
|
|
33
|
+
const [, , , region, , apiPath] = prospectusAccountsApiStageArn.split(":");
|
|
34
|
+
const [apiId, stageName] = apiPath.split("/");
|
|
35
|
+
return `https://${apiId}.execute-api.${region}.amazonaws.com/${stageName}/`;
|
|
36
|
+
}
|
|
20
37
|
function originOfRedirectUrl(value) {
|
|
21
38
|
try {
|
|
22
39
|
return new URL(value).origin;
|
|
@@ -181,7 +198,7 @@ function parseStagingPreview(value) {
|
|
|
181
198
|
}
|
|
182
199
|
function parseCms(value) {
|
|
183
200
|
const cmsRecord = requireObject(value, "cms");
|
|
184
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview"], "cms");
|
|
201
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
|
|
185
202
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
186
203
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
187
204
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -205,6 +222,7 @@ function parseCms(value) {
|
|
|
205
222
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
206
223
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
207
224
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
225
|
+
prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
|
|
208
226
|
};
|
|
209
227
|
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
210
228
|
if (reservedWord) {
|
|
@@ -212,7 +230,7 @@ function parseCms(value) {
|
|
|
212
230
|
}
|
|
213
231
|
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
214
232
|
if (key in cms.environment) {
|
|
215
|
-
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix
|
|
233
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
if (cms.staging === false && cms.stagingDomainName) {
|
package/infra/site-config.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type CmsConfig = {
|
|
|
41
41
|
stagingDomainName?: string;
|
|
42
42
|
stagingDomainPrefix?: string;
|
|
43
43
|
redirects?: CmsRedirect[];
|
|
44
|
+
prospectusAccountsApiStageArn?: string;
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
export type AmplifyAppShellSiteConfig = {
|
|
@@ -95,7 +96,26 @@ const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}
|
|
|
95
96
|
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
96
97
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
97
98
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
98
|
-
const
|
|
99
|
+
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
100
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT"];
|
|
101
|
+
|
|
102
|
+
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
103
|
+
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
104
|
+
"GET/v1/identities/me",
|
|
105
|
+
"POST/v1/identities/google/challenge",
|
|
106
|
+
"POST/v1/identities/google/resolve",
|
|
107
|
+
"GET/v1/accounts",
|
|
108
|
+
"GET/v1/accounts/*",
|
|
109
|
+
"POST/v1/accounts/*/authorize",
|
|
110
|
+
"GET/v1/account-directory",
|
|
111
|
+
"GET/v1/account-directory/*",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export function resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn: string): string {
|
|
115
|
+
const [, , , region, , apiPath] = prospectusAccountsApiStageArn.split(":");
|
|
116
|
+
const [apiId, stageName] = apiPath.split("/");
|
|
117
|
+
return `https://${apiId}.execute-api.${region}.amazonaws.com/${stageName}/`;
|
|
118
|
+
}
|
|
99
119
|
function originOfRedirectUrl(value: string): string {
|
|
100
120
|
try {
|
|
101
121
|
return new URL(value).origin;
|
|
@@ -259,7 +279,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
|
259
279
|
|
|
260
280
|
function parseCms(value: unknown): CmsConfig {
|
|
261
281
|
const cmsRecord = requireObject(value, "cms");
|
|
262
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview"], "cms");
|
|
282
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
|
|
263
283
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
264
284
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
265
285
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -288,6 +308,12 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
288
308
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
289
309
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
290
310
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
311
|
+
prospectusAccountsApiStageArn: optionalString(
|
|
312
|
+
cmsRecord.prospectusAccountsApiStageArn,
|
|
313
|
+
"cms.prospectusAccountsApiStageArn",
|
|
314
|
+
PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN,
|
|
315
|
+
"arn:aws:execute-api:<region>:<account>:<api id>/api",
|
|
316
|
+
),
|
|
291
317
|
};
|
|
292
318
|
|
|
293
319
|
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
@@ -296,7 +322,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
296
322
|
}
|
|
297
323
|
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
298
324
|
if (key in cms.environment) {
|
|
299
|
-
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix
|
|
325
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
|
|
300
326
|
}
|
|
301
327
|
}
|
|
302
328
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Priming proof reports a publication owner approved for visitors, keyed by site brand id.
|
|
3
|
+
* A report reaches this map only through a reviewed commit of its
|
|
4
|
+
* `publications/priming-proofs/<brand id>.json` file; see docs/priming-proof.md.
|
|
5
|
+
*/
|
|
6
|
+
export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {};
|