@anthusai/papyrus 2.0.0-next.87 → 2.0.0-next.89

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.
@@ -99,6 +99,11 @@ MAXIMUM_BODY = 50_000
99
99
  MAXIMUM_REASON = 500
100
100
  MAXIMUM_EVIDENCE_SOURCE = 200
101
101
  MAXIMUM_EVIDENCE_SIGNALS = 1_000_000
102
+ #: An optional figure spec kept in ``evidence.figure``: a small chart of cited series (no image), approved with the copy.
103
+ FIGURE_TYPES = ("line", "bar")
104
+ MAXIMUM_FIGURE_POINTS = 60
105
+ MAXIMUM_FIGURE_SERIES = 6
106
+ MAXIMUM_FIGURE_BYTES = 8_000
102
107
 
103
108
 
104
109
  @dataclass
@@ -190,18 +195,53 @@ def clean_targets(kind: str, raw: Any) -> list[dict[str, str]] | Outcome:
190
195
  return targets
191
196
 
192
197
 
198
+ def clean_figure(raw: Any) -> dict[str, Any] | None:
199
+ """A chart spec ``{type, title, caption, unit, source, x, series: [{signal, label, values}]}``, or None when it is malformed.
200
+
201
+ Unknown keys are dropped. Every series has exactly one value per ``x`` label; values are whole or decimal numbers, never text."""
202
+ if not isinstance(raw, dict) or raw.get("type") not in FIGURE_TYPES:
203
+ return None
204
+ title, caption, unit, source = (clean_text(raw.get(key), limit) for key, limit in (("title", 200), ("caption", 300), ("unit", 60), ("source", MAXIMUM_EVIDENCE_SOURCE)))
205
+ x, series = raw.get("x"), raw.get("series")
206
+ if None in (title, caption, unit, source) or not isinstance(x, list) or not isinstance(series, list):
207
+ return None
208
+ if not 2 <= len(x) <= MAXIMUM_FIGURE_POINTS or not 1 <= len(series) <= MAXIMUM_FIGURE_SERIES:
209
+ return None
210
+ labels = [clean_text(label, 20) for label in x]
211
+ if None in labels:
212
+ return None
213
+ cleaned = []
214
+ for entry in series:
215
+ if not isinstance(entry, dict):
216
+ return None
217
+ signal, label, values = clean_text(entry.get("signal"), 130), clean_text(entry.get("label"), 120), entry.get("values")
218
+ if signal is None or label is None or not isinstance(values, list) or len(values) != len(labels):
219
+ return None
220
+ if any(isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value <= 1_000_000_000 for value in values):
221
+ return None
222
+ cleaned.append({"signal": signal, "label": label, "values": list(values)})
223
+ figure = {"type": raw["type"], "title": title, "caption": caption, "unit": unit, "source": source, "x": labels, "series": cleaned}
224
+ return figure if len(json.dumps(figure)) <= MAXIMUM_FIGURE_BYTES else None
225
+
226
+
193
227
  def clean_evidence(raw: Any) -> dict[str, Any] | None | Outcome:
194
228
  parsed = parse_json(raw) if raw is not None else None
195
229
  if parsed is None:
196
230
  return None
197
231
  if not isinstance(parsed, dict):
198
- return refused("invalid_evidence", "evidence must be {signals, source}.")
232
+ return refused("invalid_evidence", "evidence must be {signals, source} with an optional figure.")
199
233
  signals, source = parsed.get("signals"), parsed.get("source")
200
234
  if not isinstance(signals, int) or isinstance(signals, bool) or not 0 <= signals <= MAXIMUM_EVIDENCE_SIGNALS:
201
235
  return refused("invalid_evidence", "evidence.signals must be a whole number of signals.")
202
236
  if source is not None and clean_text(source, MAXIMUM_EVIDENCE_SOURCE) is None:
203
237
  return refused("invalid_evidence", "evidence.source must be short text.")
204
- return {"signals": signals, "source": source.strip() if isinstance(source, str) else None}
238
+ evidence = {"signals": signals, "source": source.strip() if isinstance(source, str) else None}
239
+ if parsed.get("figure") is not None:
240
+ figure = clean_figure(parsed["figure"])
241
+ if figure is None:
242
+ return refused("invalid_evidence", "evidence.figure must be a small chart spec: type line or bar, title, caption, unit, source, x labels and series with one value per label.")
243
+ evidence["figure"] = figure
244
+ return evidence
205
245
 
206
246
 
207
247
  def file_proposal(arguments: dict[str, Any], *, actor_sub: str, now_iso: str, proposal_id: str | None = None) -> tuple[dict[str, Any] | None, Outcome | None]:
@@ -218,7 +218,7 @@ export function EditorialChecksAdminPanel() {
218
218
  {policies.map((policy) => (
219
219
  <tr data-editorial-policy-row={policy.id} key={policy.id}>
220
220
  <td>{policy.title}<br /><small>{policy.policyKey} · v{policy.version}</small></td>
221
- <td>{policy.scope === "publication" ? "Whole publication" : `Desk: ${policy.scope}`}</td>
221
+ <td>{policy.scope === "publication" ? "Whole publication" : `Section: ${policy.scope}`}</td>
222
222
  <td>{policy.severity}</td>
223
223
  <td>{policy.detectors.length === 0 ? "Guidance only" : policy.detectors.length}</td>
224
224
  <td>
@@ -251,7 +251,7 @@ export function EditorialChecksAdminPanel() {
251
251
  <label>Key <Input disabled={!draft.isNew} onChange={(event) => patchDraft({ policyKey: event.target.value })} value={draft.record.policyKey} /></label>
252
252
  <label>Title <Input onChange={(event) => patchDraft({ title: event.target.value })} value={draft.record.title} /></label>
253
253
  <label>
254
- Applies to (publication, or a desk key){" "}
254
+ Applies to (publication, or a section key){" "}
255
255
  <Input disabled={!draft.isNew} onChange={(event) => patchDraft({ scope: event.target.value.trim() || "publication" })} value={draft.record.scope} />
256
256
  </label>
257
257
  <label>
@@ -667,7 +667,7 @@ export function NewsDeskWorkspace({
667
667
  editorShellReady
668
668
  initialSelection={initialSelection}
669
669
  initialTab={initialTab}
670
- authState={{ status: "signedIn", label: "Demo Desk" }}
670
+ authState={{ status: "signedIn", label: "Demo section" }}
671
671
  isRefreshing={false}
672
672
  shellError={null}
673
673
  />
@@ -3903,7 +3903,7 @@ function NewsroomSectionRail({
3903
3903
  <aside className="newsroom-section-rail" data-newsroom-section-rail aria-labelledby="newsroom-section-rail-title" ref={railRef}>
3904
3904
  <header className="newsroom-section-rail__header">
3905
3905
  <p className="story-label">Sections</p>
3906
- <h2 id="newsroom-section-rail-title">Section Desk</h2>
3906
+ <h2 id="newsroom-section-rail-title">Section</h2>
3907
3907
  </header>
3908
3908
  <div className="newsroom-section-rail__body">
3909
3909
  {canonicalSections.length ? (
@@ -5743,7 +5743,7 @@ function DesksDeskView({
5743
5743
  <div className="news-desk-columns news-desk-columns--administration news-desk-columns--taxonomy" data-news-desk-section="desks">
5744
5744
  <div className="news-desk-main-column">
5745
5745
  <section className="category-steering-section category-steering-section--lead" aria-labelledby="newsroom-desks-title">
5746
- <SectionHeader title="News Desks" detail={`${roots.length} root topic desks`} />
5746
+ <SectionHeader title="News sections" detail={`${roots.length} root topic sections`} />
5747
5747
  <div className="news-desk-desk-list">
5748
5748
  {roots.length ? roots.map((root) => {
5749
5749
  const rootNode = root.node ?? categoryToCategoryTreeNode(root.category);
@@ -5763,11 +5763,11 @@ function DesksDeskView({
5763
5763
  >
5764
5764
  <span>{rootNode.shortTitle ?? deriveShortTitle(rootNode.displayName)}</span>
5765
5765
  <strong>{rootNode.displayName}</strong>
5766
- <p>{rootNode.description ?? "No desk description yet."}</p>
5766
+ <p>{rootNode.description ?? "No section description yet."}</p>
5767
5767
  <small>{referenceCount} refs / {assignmentCount} assignments / {root.subcategorys.length} subtopics / {doctrineStatus.savedCount} of 2 doctrine slots</small>
5768
5768
  </Link>
5769
5769
  );
5770
- }) : <EmptyRow label="No root topic desks are available in the canonical category set" />}
5770
+ }) : <EmptyRow label="No root topic sections are available in the canonical category set" />}
5771
5771
  </div>
5772
5772
  </section>
5773
5773
 
@@ -5788,18 +5788,18 @@ function DesksDeskView({
5788
5788
 
5789
5789
  <aside className="news-desk-rail-column">
5790
5790
  <section className="category-steering-section" aria-labelledby="desk-index-notes-title">
5791
- <SectionHeader title="Desk Rules" detail="Root topics only" />
5791
+ <SectionHeader title="Section rules" detail="Root topics only" />
5792
5792
  <div className="news-desk-ledger-list">
5793
5793
  <article className="news-desk-ledger-item">
5794
5794
  <header>
5795
- <strong>Desk Identity</strong>
5795
+ <strong>Section identity</strong>
5796
5796
  <span>Category</span>
5797
5797
  </header>
5798
5798
  <p>Official name, short title, subtitle, and description stay on the accepted root Category record.</p>
5799
5799
  </article>
5800
5800
  <article className="news-desk-ledger-item">
5801
5801
  <header>
5802
- <strong>Desk Doctrine</strong>
5802
+ <strong>Section doctrine</strong>
5803
5803
  <span>Private Item</span>
5804
5804
  </header>
5805
5805
  <p>Mission and policies are private doctrine Items tied to the root category lineage.</p>
@@ -5849,7 +5849,7 @@ function DeskDetailPanel({
5849
5849
  <section className="category-steering-section news-desk-desk-detail" aria-labelledby="selected-desk-title">
5850
5850
  <header className="news-desk-topic-detail__header">
5851
5851
  <div>
5852
- <p className="story-label">News Desk</p>
5852
+ <p className="story-label">News section</p>
5853
5853
  <h3 id="selected-desk-title">{rootNode.displayName}</h3>
5854
5854
  <span>{rootNode.shortTitle ?? deriveShortTitle(rootNode.displayName)}</span>
5855
5855
  </div>
@@ -5932,7 +5932,7 @@ function CategoryDoctrineEditorCard({
5932
5932
  <article className="news-desk-doctrine-card" data-news-desk-doctrine={definition.slug}>
5933
5933
  <header className="news-desk-doctrine-card__header">
5934
5934
  <div>
5935
- <p className="story-label">Desk Doctrine</p>
5935
+ <p className="story-label">Section doctrine</p>
5936
5936
  <h3>{definition.label}</h3>
5937
5937
  </div>
5938
5938
  <span>{record ? "Saved record" : "Empty slot"}</span>
@@ -12297,7 +12297,7 @@ function AssignmentRow({
12297
12297
  {context ? (
12298
12298
  <>
12299
12299
  <p className="news-desk-assignment-row__angle">
12300
- <span>{context.deskTitle ?? context.deskKey ?? "Desk"}</span>
12300
+ <span>{context.deskTitle ?? context.deskKey ?? "Section"}</span>
12301
12301
  {`${context.focusTitle ?? context.focusKey ?? "focus"} / ${context.targetSystemType ?? context.contextProfile ?? "context"}`}
12302
12302
  </p>
12303
12303
  {context.contextProfile || context.contextTokenBudget ? (
@@ -12412,7 +12412,7 @@ function normalizeAnalysisReindexMode(value: string | null | undefined): Analysi
12412
12412
  function formatAnalysisReindexModeLabel(mode: AnalysisReindexMode): string {
12413
12413
  if (mode === "online-update") return "Online update - new or changed accepted references";
12414
12414
  if (mode === "classifier-retrain") return "Classifier retrain - rebuild topic classifier";
12415
- if (mode === "scoped-topic-rebuild") return "Scoped topic rebuild - one desk or focus";
12415
+ if (mode === "scoped-topic-rebuild") return "Scoped topic rebuild - one section or focus";
12416
12416
  if (mode === "entity-graph-rebuild") return "Entity graph rebuild - generated graph outputs";
12417
12417
  return "Generated analysis rebuild - clear and rebuild targeted generated outputs";
12418
12418
  }
@@ -12442,7 +12442,7 @@ function mergeAnalysisCorpora(configuredCorpora: CategorySteeringCorpus[], liveC
12442
12442
  function analysisReindexModeHelp(mode: AnalysisReindexMode): string {
12443
12443
  if (mode === "generated-analysis-rebuild") return "Use this for a full generated-output rebuild for the selected profile. It still creates an assignment only; the worker/operator executes it later.";
12444
12444
  if (mode === "classifier-retrain") return "Use this when authoritative labels or accepted references should retrain the semi-supervised topic classifier.";
12445
- if (mode === "scoped-topic-rebuild") return "Use this when proposals for a desk or focus topic should be regenerated from the accepted taxonomy plus steering feedback.";
12445
+ if (mode === "scoped-topic-rebuild") return "Use this when proposals for a section or focus topic should be regenerated from the accepted taxonomy plus steering feedback.";
12446
12446
  if (mode === "entity-graph-rebuild") return "Use this when generated ontology/entity graph records should be rebuilt from accepted references.";
12447
12447
  return "Use this for incremental processing of new or changed accepted references without clearing broader generated outputs.";
12448
12448
  }
@@ -13092,7 +13092,7 @@ function formatDeskSectionHeadline(section: NewsDeskTab): string {
13092
13092
 
13093
13093
  function formatDeskSectionLede(section: NewsDeskTab): string {
13094
13094
  if (section === "search") return "Search the newsroom knowledge base with semantic and ontology context.";
13095
- if (section === "desks") return "The desks and topic categories that organize coverage, with each desk's mission and policies.";
13095
+ if (section === "desks") return "The sections and topic categories that organize coverage, with each section's mission and policies.";
13096
13096
  if (section === "topics") return "Review suggested changes to the subject areas the newsroom covers.";
13097
13097
  if (section === "concepts") return "Browse people, organizations, places, and ideas found in the source material.";
13098
13098
  if (section === "references") return "Review source materials before they become usable evidence.";
@@ -1504,7 +1504,7 @@ export function createDemoCategorySteeringDashboard(
1504
1504
  riskFlags: ["Avoid promotional framing."],
1505
1505
  coverageGaps: ["Need a second independent source."],
1506
1506
  openQuestions: ["Which users are affected first?"],
1507
- copywriterBrief: "Use accepted evidence and recent desk memory before drafting.",
1507
+ copywriterBrief: "Use accepted evidence and recent section memory before drafting.",
1508
1508
  },
1509
1509
  },
1510
1510
  },
@@ -1654,8 +1654,8 @@ export function createDemoCategorySteeringDashboard(
1654
1654
  slug: "editorial-doctrine-policy",
1655
1655
  title: "Editorial Policy",
1656
1656
  body: [
1657
- "Publication doctrine is the global inclusion standard; desk doctrine sets local source priorities and framing.",
1658
- "News reporting should prioritize current, journalistic, and official sources; desk workflows may use academic research as background unless a desk policy explicitly makes research the primary evidence class.",
1657
+ "Publication doctrine is the global inclusion standard; section doctrine sets local source priorities and framing.",
1658
+ "News reporting should prioritize current, journalistic, and official sources; section workflows may use academic research as background unless a section policy explicitly makes research the primary evidence class.",
1659
1659
  "High-risk domains, including health-related claims, require stricter sourcing, explicit uncertainty handling, and clear separation between verified facts and interpretation.",
1660
1660
  ],
1661
1661
  editorial: { kind: "policy" },
@@ -41,7 +41,7 @@ export function formatConsoleWebContextMessage(
41
41
  webPath ? `Web path: ${webPath}` : null,
42
42
  objectUri ? `Focused object: ${objectUri}` : null,
43
43
  label ? `Label: ${label}` : null,
44
- tab ? `Desk: ${tab}` : null,
44
+ tab ? `Section: ${tab}` : null,
45
45
  viewMode ? `View: ${viewMode}` : null,
46
46
  ].filter((line): line is string => Boolean(line));
47
47
 
@@ -237,7 +237,7 @@ export function applyPilobolusDemoDashboardContent(
237
237
  ...record,
238
238
  title: "Pilobolus Editorial Policy",
239
239
  body: [
240
- "Publication doctrine is the global inclusion standard for Pilobol.us; desk doctrine sets local intake priorities for field notes and source intake.",
240
+ "Publication doctrine is the global inclusion standard for Pilobol.us; section doctrine sets local intake priorities for field notes and source intake.",
241
241
  "Field-notes reporting should prioritize accepted Pilobolus references and annotated intake artifacts—not the Papyrus platform knowledge base.",
242
242
  "High-risk claims about host takeover or network control require explicit uncertainty handling and accepted source linkage.",
243
243
  ],
@@ -21,7 +21,7 @@ export type NewsroomNavItem = {
21
21
  };
22
22
 
23
23
  export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
24
- { id: "overview", label: "Overview", detail: "Desk home", href: newsroomHref() },
24
+ { id: "overview", label: "Overview", detail: "Newsroom home", href: newsroomHref() },
25
25
  { id: "articles", label: getContentItemName().plural, detail: "Drafts & published", href: newsroomHref("articles") },
26
26
  { id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments") },
27
27
  { id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references") },
package/package.json CHANGED
@@ -448,7 +448,7 @@
448
448
  "optional": true
449
449
  }
450
450
  },
451
- "version": "2.0.0-next.87",
451
+ "version": "2.0.0-next.89",
452
452
  "dependencies": {
453
453
  "@aws-amplify/adapter-nextjs": "^1.7.3",
454
454
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.0-next.87",
2
+ "version": "2.0.0-next.89",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",