@anthusai/papyrus 1.16.0 → 1.17.0

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.
@@ -2190,7 +2190,7 @@ fn build_openai_messages(
2190
2190
  ) -> Vec<Value> {
2191
2191
  let mut messages = vec![json!({
2192
2192
  "role": "system",
2193
- "content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.edition-candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
2193
+ "content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
2194
2194
  })];
2195
2195
  if !static_prompt.publication_mission.trim().is_empty() {
2196
2196
  messages.push(json!({
@@ -3,7 +3,7 @@ import { getSiteRenderer } from "../../../../../lib/site-renderer";
3
3
  import { getCachedEditionItem, loadCachedEditionContent } from "../../../../../lib/cached-content-repository";
4
4
  import { getEditionDatePath, parseEditionArticleRoute } from "../../../../../lib/edition-routes";
5
5
  import { buildPresentationFooterEntries } from "../../../../../lib/presentation-footer";
6
- import { SITE_BRAND } from "../../../../../lib/site-brand";
6
+ import { SITE_BRAND, editionsEnabled } from "../../../../../lib/site-brand";
7
7
  import { generateEditionItemStaticParams } from "../../../../../lib/reader-static-params";
8
8
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
9
9
  export const revalidate = 60;
@@ -39,6 +39,7 @@ export async function generateMetadata({ params }: DateScopedArticlePageProps) {
39
39
  }
40
40
 
41
41
  export default async function DateScopedArticlePage({ params }: DateScopedArticlePageProps) {
42
+ if (!editionsEnabled()) notFound();
42
43
  const { year, month, day, articleSlug } = await params;
43
44
  const route = parseEditionArticleRoute({ year, month, day, articleSlug });
44
45
  if (!route) notFound();
@@ -1,4 +1,5 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
+ import { editionsEnabled } from "../../../../lib/site-brand";
2
3
  import { siteServesNextReaderRoutes } from "../../../../lib/reader-static-params";
3
4
  import { getSiteRenderer } from "../../../../lib/site-renderer";
4
5
  import type { PresentationTarget } from "../../../../renderers/pretext";
@@ -23,7 +24,7 @@ export async function EditionRoutePage({
23
24
  lockedPresentation,
24
25
  target = { kind: "edition" },
25
26
  }: EditionRoutePageProps) {
26
- if (!siteServesNextReaderRoutes()) notFound();
27
+ if (!editionsEnabled() || !siteServesNextReaderRoutes()) notFound();
27
28
  const route = parseEditionDateRoute({ year, month, day });
28
29
  if (!route) notFound();
29
30
  if (!route.isCanonical) redirect(route.canonicalPath);
@@ -1,7 +1,7 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
2
  import { EditionRoutePage } from "../../edition-route-page";
3
3
  import { parseEditionPageRoute } from "../../../../../../lib/edition-routes";
4
- import { getDefaultPretextLayout, getForcedPresentation } from "../../../../../../lib/site-brand";
4
+ import { editionsEnabled, getDefaultPretextLayout, getForcedPresentation } from "../../../../../../lib/site-brand";
5
5
  import { generateEditionPageStaticParams } from "../../../../../../lib/reader-static-params";
6
6
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
7
7
  export const revalidate = 60;
@@ -20,6 +20,7 @@ type EditionPageRouteProps = {
20
20
  };
21
21
 
22
22
  export default async function EditionPageRoute({ params }: EditionPageRouteProps) {
23
+ if (!editionsEnabled()) notFound();
23
24
  const { year, month, day, pageNumber } = await params;
24
25
  const route = parseEditionPageRoute({ year, month, day, pageNumber });
25
26
  if (!route) notFound();
@@ -1,3 +1,5 @@
1
+ import { notFound } from "next/navigation";
2
+ import { editionsEnabled } from "../../../../lib/site-brand";
1
3
  import { EditionRoutePage } from "./edition-route-page";
2
4
  import { generateEditionDateStaticParams } from "../../../../lib/reader-static-params";
3
5
 
@@ -17,6 +19,7 @@ type DateEditionPageProps = {
17
19
  };
18
20
 
19
21
  export default async function DateEditionPage({ params }: DateEditionPageProps) {
22
+ if (!editionsEnabled()) notFound();
20
23
  const { year, month, day } = await params;
21
24
  return <EditionRoutePage day={day} month={month} year={year} />;
22
25
  }
@@ -2,6 +2,7 @@ import { notFound, redirect } from "next/navigation";
2
2
  import { EditionRoutePage } from "../../edition-route-page";
3
3
  import { createSectionKey } from "../../../../../../lib/edition-sections";
4
4
  import { parseEditionSectionRoute } from "../../../../../../lib/edition-routes";
5
+ import { editionsEnabled } from "../../../../../../lib/site-brand";
5
6
  import { generateEditionSectionStaticParams } from "../../../../../../lib/reader-static-params";
6
7
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
7
8
  export const revalidate = 60;
@@ -20,6 +21,7 @@ type EditionSectionRouteProps = {
20
21
  };
21
22
 
22
23
  export default async function EditionSectionRoute({ params }: EditionSectionRouteProps) {
24
+ if (!editionsEnabled()) notFound();
23
25
  const { year, month, day, sectionKey } = await params;
24
26
  const normalizedSectionKey = createSectionKey(sectionKey);
25
27
  const route = parseEditionSectionRoute({ year, month, day, sectionKey: normalizedSectionKey });
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { editionsEnabled } from "../../../../lib/site-brand";
2
3
  import { loadArchiveEditionPreviews, clampArchiveLimit } from "../../../../lib/archive-data";
3
4
  import { ARCHIVE_BATCH_SIZE } from "../../../../lib/archive-types";
4
5
 
@@ -6,6 +7,7 @@ import { ARCHIVE_BATCH_SIZE } from "../../../../lib/archive-types";
6
7
  export const revalidate = 60;
7
8
 
8
9
  export async function GET(request: Request) {
10
+ if (!editionsEnabled()) return new NextResponse(null, { status: 404 });
9
11
  const url = new URL(request.url);
10
12
  const cursor = url.searchParams.get("cursor");
11
13
  const requestedLimit = Number(url.searchParams.get("limit") ?? ARCHIVE_BATCH_SIZE);
@@ -1,3 +1,5 @@
1
+ import { notFound } from "next/navigation";
2
+ import { editionsEnabled } from "../../lib/site-brand";
1
3
  import { ArchiveShell } from "../../components/archive-shell";
2
4
  import { ArchiveGrid } from "../../components/archive-grid";
3
5
  import { loadArchiveEditionPreviews } from "../../lib/archive-data";
@@ -7,6 +9,7 @@ import { ARCHIVE_BATCH_SIZE } from "../../lib/archive-types";
7
9
  export const revalidate = 60;
8
10
 
9
11
  export default async function ArchivePage() {
12
+ if (!editionsEnabled()) notFound();
10
13
  const initialBatch = await loadArchiveEditionPreviews({ limit: ARCHIVE_BATCH_SIZE });
11
14
 
12
15
  return (
package/app/page.tsx CHANGED
@@ -15,7 +15,10 @@ import {
15
15
  type PublicPlaceholderTopic,
16
16
  } from "../lib/public-placeholder-config";
17
17
  import type { PublicationItem } from "../lib/publication-items";
18
- import { SITE_BRAND, rootRoute } from "../lib/site-brand";
18
+ import { SITE_BRAND, editionsEnabled, rootRoute } from "../lib/site-brand";
19
+ import { ItemStreamView } from "../components/item-stream-view";
20
+ import { ITEM_STREAM_RECENT_EDITION_COUNT, buildItemStream, selectNewestStreamItems } from "../lib/item-stream";
21
+ import { listCachedPublishedEditions } from "../lib/cached-content-repository";
19
22
  import { notFound, redirect } from "next/navigation";
20
23
  import { getReaderBasePath } from "../lib/reader-base-path";
21
24
  import { buildRootRedirectTarget } from "../lib/root-route-redirect";
@@ -27,6 +30,7 @@ export const dynamic = "force-dynamic";
27
30
  type HomePageProps = {
28
31
  searchParams?: Promise<{
29
32
  scenario?: string | string[];
33
+ section?: string | string[];
30
34
  code?: string | string[];
31
35
  state?: string | string[];
32
36
  error?: string | string[];
@@ -59,6 +63,13 @@ export default async function Home({ searchParams }: HomePageProps) {
59
63
  notFound();
60
64
  }
61
65
 
66
+ if (!editionsEnabled()) {
67
+ const sectionParam = resolvedSearchParams?.section;
68
+ const sectionKey = Array.isArray(sectionParam) ? sectionParam[0] : sectionParam;
69
+ const items = await loadNewestStreamItemsOrNone();
70
+ return <ItemStreamView stream={buildItemStream(items, sectionKey)} />;
71
+ }
72
+
62
73
  // Resolved only on the reader path: Markus brands (renderer "markus") have
63
74
  // no Next.js reader and getSiteRenderer() throws for them, but they can
64
75
  // still serve the newsroom at the root via rootRoute above.
@@ -88,6 +99,24 @@ export default async function Home({ searchParams }: HomePageProps) {
88
99
  return <siteRenderer.renderEdition content={content} />;
89
100
  }
90
101
 
102
+ async function loadNewestStreamItemsOrNone() {
103
+ try {
104
+ return await loadNewestStreamItems();
105
+ } catch (error) {
106
+ if (!isMissingReaderBackendError(error)) throw error;
107
+ return [];
108
+ }
109
+ }
110
+
111
+ async function loadNewestStreamItems() {
112
+ const { editions } = await listCachedPublishedEditions({ limit: ITEM_STREAM_RECENT_EDITION_COUNT });
113
+ const newestFirst = [...editions].sort((left, right) => right.editionDate.localeCompare(left.editionDate));
114
+ const contents = await Promise.all(
115
+ newestFirst.map((edition) => loadCachedEditionContent({ editionDate: edition.editionDate })),
116
+ );
117
+ return selectNewestStreamItems(contents);
118
+ }
119
+
91
120
  async function loadLatestGraphQLEdition(): Promise<EditionContent | null> {
92
121
  const latestEdition = await getCachedLatestPublishedEdition();
93
122
  if (!latestEdition) return null;
@@ -0,0 +1,63 @@
1
+ import Link from "next/link";
2
+ import type { ItemStream } from "../lib/item-stream";
3
+ import { toPublicReaderPath } from "../lib/reader-base-path";
4
+ import { SITE_BRAND } from "../lib/site-brand";
5
+
6
+ /** The home of a publication without editions: a masthead, section filters and the newest items. */
7
+ export function ItemStreamView({ stream }: { stream: ItemStream }) {
8
+ const homePath = toPublicReaderPath("/");
9
+ const tagline = SITE_BRAND.mastheadTagline ?? null;
10
+
11
+ return (
12
+ <main className="presentation-page presentation-page--blog" data-presentation-engine="item-stream">
13
+ <header className="presentation-header">
14
+ <div className="presentation-header__copy-stack">
15
+ <h1>
16
+ <Link href={homePath}>{SITE_BRAND.mastheadTitle}</Link>
17
+ </h1>
18
+ <div className="presentation-header__meta">
19
+ <span className="presentation-header__subtitle">{SITE_BRAND.mastheadSubtitle}</span>
20
+ </div>
21
+ {tagline ? <span className="presentation-header__tagline">{tagline}</span> : null}
22
+ </div>
23
+ </header>
24
+ {stream.sections.length > 1 ? (
25
+ <nav className="presentation-section-nav" aria-label="Sections">
26
+ <Link href={homePath} aria-current={stream.activeSectionKey === null ? "page" : undefined}>
27
+ All
28
+ </Link>
29
+ {stream.sections.map((section) => (
30
+ <Link
31
+ aria-current={stream.activeSectionKey === section.key ? "page" : undefined}
32
+ href={`${homePath}?section=${encodeURIComponent(section.key)}`}
33
+ key={section.key}
34
+ >
35
+ {section.label}
36
+ </Link>
37
+ ))}
38
+ </nav>
39
+ ) : null}
40
+ {stream.items.length === 0 ? (
41
+ <section className="item-stream-empty" data-item-stream-empty="true">
42
+ <p>Nothing published yet</p>
43
+ </section>
44
+ ) : (
45
+ <div className="blog-sections">
46
+ {stream.items.map((item) => (
47
+ <article className="presentation-item presentation-item--blog" data-item-id={item.slug} key={item.slug}>
48
+ <div className="presentation-item__copy">
49
+ <header className="presentation-item__header">
50
+ <p>{item.section ?? "General"}</p>
51
+ <h2>
52
+ <Link href={toPublicReaderPath(`/articles/${encodeURIComponent(item.slug)}`)}>{item.headline}</Link>
53
+ </h2>
54
+ {item.deck ? <span>{item.deck}</span> : null}
55
+ </header>
56
+ </div>
57
+ </article>
58
+ ))}
59
+ </div>
60
+ )}
61
+ </main>
62
+ );
63
+ }
@@ -117,7 +117,7 @@ function AssignmentDetailPanel({
117
117
  detailExtras?: ReactNode;
118
118
  }) {
119
119
  const terminal = assignment.status === "completed" || assignment.status === "canceled";
120
- const isReporting = assignment.assignmentTypeKey === "reporting.edition-candidate";
120
+ const isReporting = assignment.assignmentTypeKey === "reporting.candidate";
121
121
 
122
122
  return (
123
123
  <div
@@ -12368,7 +12368,7 @@ function ReportingStoryBudgetBoard({
12368
12368
  section={section}
12369
12369
  selectedAssignmentId={selectedAssignmentId}
12370
12370
  />
12371
- )) : <EmptyRow label="No reporting edition-candidate assignments found" />}
12371
+ )) : <EmptyRow label="No reporting candidate assignments found" />}
12372
12372
  </div>
12373
12373
  );
12374
12374
  }
@@ -14568,7 +14568,7 @@ function buildUiReportingPacketReviewPlan({
14568
14568
  event: AssignmentEventRecord;
14569
14569
  relations: SemanticRelationRecord[];
14570
14570
  } {
14571
- if (assignment.assignmentTypeKey !== "reporting.edition-candidate") throw new Error("Only reporting edition-candidate assignments can review reporting packets.");
14571
+ if (assignment.assignmentTypeKey !== "reporting.candidate") throw new Error("Only reporting candidate assignments can review reporting packets.");
14572
14572
  if (message.messageKind !== "reporting_context_packet") throw new Error("Only reporting context packets can be reviewed.");
14573
14573
  if (decision === "merge" && !targetItem?.id && !targetItemId.trim()) throw new Error("Merge Packet requires a target Item ID.");
14574
14574
  const resolvedTargetItem = targetItem ?? (targetItemId.trim() ? { id: targetItemId.trim(), lineageId: targetItemId.trim(), versionNumber: null } : null);
@@ -1510,7 +1510,7 @@ export function createDemoCategorySteeringDashboard(
1510
1510
  assignments: [
1511
1511
  {
1512
1512
  id: assignmentReportingId,
1513
- assignmentTypeKey: "reporting.edition-candidate",
1513
+ assignmentTypeKey: "reporting.candidate",
1514
1514
  queueKey: "edition:edition-demo:section:news:lane:reporting",
1515
1515
  queueStatusKey: "edition:edition-demo:section:news:lane:reporting#open",
1516
1516
  status: "open",
@@ -0,0 +1,47 @@
1
+ import type { EditionContent, EditionSection } from "./content-types";
2
+ import { createEditionSectionPlan, createSectionKey } from "./edition-sections";
3
+ import { getArticlePublicationItems, type ArticlePublicationItem } from "./publication-items";
4
+
5
+ export const ITEM_STREAM_RECENT_EDITION_COUNT = 10;
6
+ export const ITEM_STREAM_ITEM_LIMIT = 50;
7
+
8
+ export type ItemStream = {
9
+ items: ArticlePublicationItem[];
10
+ sections: EditionSection[];
11
+ activeSectionKey: string | null;
12
+ };
13
+
14
+ /**
15
+ * Flattens published content into one newest-first stream of articles. The
16
+ * input is ordered newest first; a slug that appears twice keeps its newest entry.
17
+ */
18
+ export function selectNewestStreamItems(
19
+ newestFirstContents: EditionContent[],
20
+ limit: number = ITEM_STREAM_ITEM_LIMIT,
21
+ ): ArticlePublicationItem[] {
22
+ const seenSlugs = new Set<string>();
23
+ const selected: ArticlePublicationItem[] = [];
24
+ for (const content of newestFirstContents) {
25
+ for (const item of getArticlePublicationItems(content.items)) {
26
+ if (seenSlugs.has(item.slug)) continue;
27
+ seenSlugs.add(item.slug);
28
+ selected.push(item);
29
+ if (selected.length >= limit) return selected;
30
+ }
31
+ }
32
+ return selected;
33
+ }
34
+
35
+ /** Builds the stream view: the sections found in the items, and the items filtered by an optional section key. */
36
+ export function buildItemStream(items: ArticlePublicationItem[], sectionKey?: string | null): ItemStream {
37
+ const sections = createEditionSectionPlan(items);
38
+ const activeSectionKey = sectionKey ? createSectionKey(sectionKey) : null;
39
+ const activeSection = activeSectionKey ? sections.find((section) => section.key === activeSectionKey) : undefined;
40
+ if (!activeSection) return { items, sections, activeSectionKey: null };
41
+ const itemIds = new Set(activeSection.itemIds);
42
+ return {
43
+ items: items.filter((item) => itemIds.has(item.slug)),
44
+ sections,
45
+ activeSectionKey: activeSection.key,
46
+ };
47
+ }
@@ -41,7 +41,7 @@ export function formatAssignmentTypeLabel(typeKey: string | null | undefined): s
41
41
  if (key === "analysis.reindex") return "Analysis re-index";
42
42
  if (key === "newsroom.research" || key === "research") return "Research";
43
43
  if (key === "curation.reference-intake") return "Reference intake";
44
- if (key === "reporting.edition-candidate") return "Reporting candidate";
44
+ if (key === "reporting.candidate") return "Reporting candidate";
45
45
  return key
46
46
  .split(/[.:_-]+/)
47
47
  .filter(Boolean)
@@ -194,7 +194,7 @@ export function applyPilobolusDemoDashboardContent(
194
194
  });
195
195
 
196
196
  localized.assignments = localized.assignments.map((assignment, index) => {
197
- if (assignment.assignmentTypeKey === "reporting.edition-candidate") {
197
+ if (assignment.assignmentTypeKey === "reporting.candidate") {
198
198
  return {
199
199
  ...assignment,
200
200
  title: "Field-notes reporting candidate 1: Strangler-fig control-loop shift",
@@ -1,6 +1,6 @@
1
1
  import type { ContentRepository, EditionContent } from "./content-types";
2
2
  import { graphqlContentRepository } from "./graphql-content-repository";
3
- import { SITE_BRAND } from "./site-brand";
3
+ import { SITE_BRAND, editionsEnabled } from "./site-brand";
4
4
  import { getContentSource } from "./site-env";
5
5
  import type { RendererConfig } from "./renderer-config";
6
6
 
@@ -40,6 +40,7 @@ const RESERVED_DATE_CHILD_SEGMENTS = new Set(["page", "section"]);
40
40
  type EnumerationRepository = Pick<ContentRepository, "listPublishedEditions" | "loadEditionContent">;
41
41
 
42
42
  function shouldEnumerateAtBuild(): boolean {
43
+ if (!editionsEnabled()) return false;
43
44
  if (process.env.NODE_ENV === "development") return false;
44
45
  return siteEnumeratesReaderRoutesAtBuild();
45
46
  }
@@ -106,7 +106,7 @@ export type ReportingStoryBudgetInput = {
106
106
  messagePayloads?: Record<string, unknown>;
107
107
  };
108
108
 
109
- const REPORTING_ASSIGNMENT_TYPE = "reporting.edition-candidate";
109
+ const REPORTING_ASSIGNMENT_TYPE = "reporting.candidate";
110
110
  const REPORTING_PACKET_KIND = "reporting_context_packet";
111
111
  const RESEARCH_PACKET_KIND = "research_packet";
112
112
 
@@ -331,7 +331,7 @@ function buildResearchPacketIdsByReportingAssignment(assignments: AssignmentReco
331
331
  if (relation.subjectKind === "assignment" && relation.objectKind === "assignment") {
332
332
  const reporting = assignmentById.get(relation.subjectId);
333
333
  const research = assignmentById.get(relation.objectId);
334
- if (reporting?.assignmentTypeKey !== REPORTING_ASSIGNMENT_TYPE || research?.assignmentTypeKey !== "research.edition-candidate") continue;
334
+ if (reporting?.assignmentTypeKey !== REPORTING_ASSIGNMENT_TYPE || research?.assignmentTypeKey !== "research.candidate") continue;
335
335
  for (const packetId of producedResearchPacketsByAssignment.get(research.id) ?? []) {
336
336
  result.set(reporting.id, addToSet(result.get(reporting.id), packetId));
337
337
  }
package/lib/site-brand.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isLocalPreviewEnabled } from "./local-preview";
2
+ import { areEditionsEnabled } from "./site-env";
2
3
  import type { ComponentType } from "react";
3
4
  import type { EditionPresentationFormat } from "./content-types";
4
5
  import type { Article, ArticleVideoAsset } from "./articles";
@@ -157,6 +158,11 @@ export type SiteBrand = {
157
158
  fonts?: SiteBrandFont[];
158
159
  demoEdition?: SiteBrandDemoEdition;
159
160
  video?: SiteBrandVideo;
161
+ /**
162
+ * Editions are an optional setting, off unless `{ enabled: true }`. When off the
163
+ * publication is a plain stream of items: no edition date routes, archive or masthead date.
164
+ */
165
+ editions?: { enabled: boolean };
160
166
  };
161
167
 
162
168
  const SERIF_TEXT_FONT = 'Georgia, "Times New Roman", serif';
@@ -253,6 +259,11 @@ export function getRootRoute(brand: SiteBrand = SITE_BRAND): RootRouteConfig {
253
259
 
254
260
  export const rootRoute = getRootRoute();
255
261
 
262
+ /** The brand setting wins when present; otherwise the PAPYRUS_EDITIONS_ENABLED switch decides. */
263
+ export function editionsEnabled(brand: Pick<SiteBrand, "editions"> = SITE_BRAND): boolean {
264
+ return brand.editions?.enabled ?? areEditionsEnabled();
265
+ }
266
+
256
267
  export function enforcePresentation(presentation: EditionPresentationFormat): EditionPresentationFormat {
257
268
  return SITE_BRAND.forcedPresentation ?? presentation;
258
269
  }
package/lib/site-env.ts CHANGED
@@ -20,6 +20,11 @@ export function getSiteEnv(environment: EnvironmentVariables = process.env): Sit
20
20
  return configured as SiteEnv;
21
21
  }
22
22
 
23
+ export function areEditionsEnabled(environment: EnvironmentVariables = process.env): boolean {
24
+ const configured = (environment.PAPYRUS_EDITIONS_ENABLED ?? "").trim().toLowerCase();
25
+ return configured === "true" || configured === "1" || configured === "yes" || configured === "on";
26
+ }
27
+
23
28
  export function getContentSource(environment: EnvironmentVariables = process.env): ContentSource {
24
29
  const configured = (environment.PAPYRUS_CONTENT_SOURCE ?? "").trim();
25
30
  if (!configured) return "published";
package/package.json CHANGED
@@ -78,6 +78,7 @@
78
78
  "./components/blog-page-background": "./components/blog-page-background.tsx",
79
79
  "./components/curation-delegation-client": "./components/curation-delegation-client.ts",
80
80
  "./components/dev-sandbox-editor-sign-in": "./components/dev-sandbox-editor-sign-in.tsx",
81
+ "./components/item-stream-view": "./components/item-stream-view.tsx",
81
82
  "./components/markus-ir-preview": "./components/markus-ir-preview.tsx",
82
83
  "./components/news-desk-client-provider": "./components/news-desk-client-provider.tsx",
83
84
  "./components/news-desk-page": "./components/news-desk-page.tsx",
@@ -191,6 +192,7 @@
191
192
  "./lib/furniture": "./lib/furniture.ts",
192
193
  "./lib/graphql-content-repository": "./lib/graphql-content-repository.ts",
193
194
  "./lib/image-url": "./lib/image-url.ts",
195
+ "./lib/item-stream": "./lib/item-stream.ts",
194
196
  "./lib/labels": "./lib/labels.ts",
195
197
  "./lib/layout-config": "./lib/layout-config.ts",
196
198
  "./lib/layout-plan": "./lib/layout-plan.ts",
@@ -377,7 +379,7 @@
377
379
  "optional": true
378
380
  }
379
381
  },
380
- "version": "1.16.0",
382
+ "version": "1.17.0",
381
383
  "dependencies": {
382
384
  "@aws-amplify/adapter-nextjs": "^1.7.3",
383
385
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.16.0",
2
+ "version": "1.17.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",