@anthusai/papyrus 2.0.0-next.88 → 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]:
package/package.json CHANGED
@@ -448,7 +448,7 @@
448
448
  "optional": true
449
449
  }
450
450
  },
451
- "version": "2.0.0-next.88",
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.88",
2
+ "version": "2.0.0-next.89",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",