@zenalexa/unicli 0.225.2 → 0.225.3

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.
Files changed (184) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/adapters/acl-anthology/papers.d.ts +16 -9
  5. package/dist/adapters/acl-anthology/papers.d.ts.map +1 -1
  6. package/dist/adapters/acl-anthology/papers.js +322 -58
  7. package/dist/adapters/acl-anthology/papers.js.map +1 -1
  8. package/dist/adapters/arxiv/papers.d.ts +22 -4
  9. package/dist/adapters/arxiv/papers.d.ts.map +1 -1
  10. package/dist/adapters/arxiv/papers.js +202 -4
  11. package/dist/adapters/arxiv/papers.js.map +1 -1
  12. package/dist/adapters/baidu-scholar/search.d.ts +15 -1
  13. package/dist/adapters/baidu-scholar/search.d.ts.map +1 -1
  14. package/dist/adapters/baidu-scholar/search.js +72 -8
  15. package/dist/adapters/baidu-scholar/search.js.map +1 -1
  16. package/dist/adapters/biorxiv/preprints.d.ts +9 -0
  17. package/dist/adapters/biorxiv/preprints.d.ts.map +1 -0
  18. package/dist/adapters/biorxiv/preprints.js +78 -0
  19. package/dist/adapters/biorxiv/preprints.js.map +1 -0
  20. package/dist/adapters/cnki/search.d.ts +82 -0
  21. package/dist/adapters/cnki/search.d.ts.map +1 -0
  22. package/dist/adapters/cnki/search.js +236 -0
  23. package/dist/adapters/cnki/search.js.map +1 -0
  24. package/dist/adapters/cvf/papers.d.ts +12 -7
  25. package/dist/adapters/cvf/papers.d.ts.map +1 -1
  26. package/dist/adapters/cvf/papers.js +210 -27
  27. package/dist/adapters/cvf/papers.js.map +1 -1
  28. package/dist/adapters/dblp/publications.d.ts +12 -5
  29. package/dist/adapters/dblp/publications.d.ts.map +1 -1
  30. package/dist/adapters/dblp/publications.js +31 -8
  31. package/dist/adapters/dblp/publications.js.map +1 -1
  32. package/dist/adapters/google-scholar/search.d.ts +22 -1
  33. package/dist/adapters/google-scholar/search.d.ts.map +1 -1
  34. package/dist/adapters/google-scholar/search.js +129 -14
  35. package/dist/adapters/google-scholar/search.js.map +1 -1
  36. package/dist/adapters/hf/paper.d.ts +12 -3
  37. package/dist/adapters/hf/paper.d.ts.map +1 -1
  38. package/dist/adapters/hf/paper.js +65 -5
  39. package/dist/adapters/hf/paper.js.map +1 -1
  40. package/dist/adapters/medrxiv/preprints.d.ts +9 -0
  41. package/dist/adapters/medrxiv/preprints.d.ts.map +1 -0
  42. package/dist/adapters/medrxiv/preprints.js +78 -0
  43. package/dist/adapters/medrxiv/preprints.js.map +1 -0
  44. package/dist/adapters/neurips/proceedings.d.ts +8 -7
  45. package/dist/adapters/neurips/proceedings.d.ts.map +1 -1
  46. package/dist/adapters/neurips/proceedings.js +209 -21
  47. package/dist/adapters/neurips/proceedings.js.map +1 -1
  48. package/dist/adapters/openalex/works.d.ts +21 -5
  49. package/dist/adapters/openalex/works.d.ts.map +1 -1
  50. package/dist/adapters/openalex/works.js +108 -8
  51. package/dist/adapters/openalex/works.js.map +1 -1
  52. package/dist/adapters/openreview/papers.d.ts +10 -4
  53. package/dist/adapters/openreview/papers.d.ts.map +1 -1
  54. package/dist/adapters/openreview/papers.js +351 -24
  55. package/dist/adapters/openreview/papers.js.map +1 -1
  56. package/dist/adapters/pmlr/proceedings.d.ts +6 -6
  57. package/dist/adapters/pmlr/proceedings.d.ts.map +1 -1
  58. package/dist/adapters/pmlr/proceedings.js +92 -12
  59. package/dist/adapters/pmlr/proceedings.js.map +1 -1
  60. package/dist/adapters/pubmed/articles.d.ts +8 -4
  61. package/dist/adapters/pubmed/articles.d.ts.map +1 -1
  62. package/dist/adapters/pubmed/articles.js +272 -39
  63. package/dist/adapters/pubmed/articles.js.map +1 -1
  64. package/dist/adapters/rxiv/preprints.d.ts +75 -0
  65. package/dist/adapters/rxiv/preprints.d.ts.map +1 -0
  66. package/dist/adapters/rxiv/preprints.js +651 -0
  67. package/dist/adapters/rxiv/preprints.js.map +1 -0
  68. package/dist/adapters/scholar-artifacts/pdf-read.d.ts +49 -0
  69. package/dist/adapters/scholar-artifacts/pdf-read.d.ts.map +1 -0
  70. package/dist/adapters/scholar-artifacts/pdf-read.js +204 -0
  71. package/dist/adapters/scholar-artifacts/pdf-read.js.map +1 -0
  72. package/dist/adapters/scholar-artifacts/pdf.d.ts +16 -0
  73. package/dist/adapters/scholar-artifacts/pdf.d.ts.map +1 -0
  74. package/dist/adapters/scholar-artifacts/pdf.js +122 -0
  75. package/dist/adapters/scholar-artifacts/pdf.js.map +1 -0
  76. package/dist/adapters/semantic-scholar/papers.d.ts +6 -6
  77. package/dist/adapters/semantic-scholar/papers.d.ts.map +1 -1
  78. package/dist/adapters/semantic-scholar/papers.js +80 -6
  79. package/dist/adapters/semantic-scholar/papers.js.map +1 -1
  80. package/dist/adapters/unpaywall/works.d.ts +7 -7
  81. package/dist/adapters/unpaywall/works.d.ts.map +1 -1
  82. package/dist/adapters/unpaywall/works.js +104 -12
  83. package/dist/adapters/unpaywall/works.js.map +1 -1
  84. package/dist/adapters/wanfang/search.d.ts +14 -0
  85. package/dist/adapters/wanfang/search.d.ts.map +1 -1
  86. package/dist/adapters/wanfang/search.js +56 -7
  87. package/dist/adapters/wanfang/search.js.map +1 -1
  88. package/dist/browser/page.d.ts +2 -0
  89. package/dist/browser/page.d.ts.map +1 -1
  90. package/dist/browser/page.js +12 -0
  91. package/dist/browser/page.js.map +1 -1
  92. package/dist/commands/browser/actions.d.ts.map +1 -1
  93. package/dist/commands/browser/actions.js +59 -3
  94. package/dist/commands/browser/actions.js.map +1 -1
  95. package/dist/commands/scholar.d.ts +77 -5
  96. package/dist/commands/scholar.d.ts.map +1 -1
  97. package/dist/commands/scholar.js +2945 -83
  98. package/dist/commands/scholar.js.map +1 -1
  99. package/dist/core/command-contract.d.ts.map +1 -1
  100. package/dist/core/command-contract.js +5 -0
  101. package/dist/core/command-contract.js.map +1 -1
  102. package/dist/core/schema-v2.d.ts +1 -0
  103. package/dist/core/schema-v2.d.ts.map +1 -1
  104. package/dist/core/schema-v2.js +1 -0
  105. package/dist/core/schema-v2.js.map +1 -1
  106. package/dist/discovery/aliases.d.ts.map +1 -1
  107. package/dist/discovery/aliases.js +208 -0
  108. package/dist/discovery/aliases.js.map +1 -1
  109. package/dist/discovery/core-catalog.d.ts +2 -0
  110. package/dist/discovery/core-catalog.d.ts.map +1 -1
  111. package/dist/discovery/core-catalog.js +487 -0
  112. package/dist/discovery/core-catalog.js.map +1 -1
  113. package/dist/discovery/intents.d.ts.map +1 -1
  114. package/dist/discovery/intents.js +273 -2
  115. package/dist/discovery/intents.js.map +1 -1
  116. package/dist/discovery/loader.d.ts.map +1 -1
  117. package/dist/discovery/loader.js +3 -0
  118. package/dist/discovery/loader.js.map +1 -1
  119. package/dist/engine/capability-policy.d.ts.map +1 -1
  120. package/dist/engine/capability-policy.js +30 -4
  121. package/dist/engine/capability-policy.js.map +1 -1
  122. package/dist/engine/kernel/stages.d.ts.map +1 -1
  123. package/dist/engine/kernel/stages.js +3 -0
  124. package/dist/engine/kernel/stages.js.map +1 -1
  125. package/dist/engine/operation-policy.d.ts +4 -1
  126. package/dist/engine/operation-policy.d.ts.map +1 -1
  127. package/dist/engine/operation-policy.js +23 -0
  128. package/dist/engine/operation-policy.js.map +1 -1
  129. package/dist/fast-path/manifest.d.ts +3 -0
  130. package/dist/fast-path/manifest.d.ts.map +1 -1
  131. package/dist/fast-path/manifest.js.map +1 -1
  132. package/dist/fast-path/policy.d.ts.map +1 -1
  133. package/dist/fast-path/policy.js +3 -0
  134. package/dist/fast-path/policy.js.map +1 -1
  135. package/dist/manifest-compact.txt +1 -1
  136. package/dist/manifest.json +6804 -1002
  137. package/dist/registry.d.ts +2 -0
  138. package/dist/registry.d.ts.map +1 -1
  139. package/dist/registry.js +1 -0
  140. package/dist/registry.js.map +1 -1
  141. package/dist/types/scholarly.d.ts +19 -4
  142. package/dist/types/scholarly.d.ts.map +1 -1
  143. package/dist/types/scholarly.js +4 -4
  144. package/dist/types.d.ts +8 -0
  145. package/dist/types.d.ts.map +1 -1
  146. package/dist/types.js.map +1 -1
  147. package/package.json +1 -1
  148. package/server.json +2 -2
  149. package/skills/unicli/SKILL.md +1 -1
  150. package/skills/unicli-claude-code/SKILL.md +1 -1
  151. package/skills/unicli-hermes/SKILL.md +1 -1
  152. package/src/adapters/acl-anthology/papers.test.ts +111 -0
  153. package/src/adapters/acl-anthology/papers.ts +379 -71
  154. package/src/adapters/arxiv/papers.test.ts +46 -0
  155. package/src/adapters/arxiv/papers.ts +251 -4
  156. package/src/adapters/baidu-scholar/search.ts +74 -11
  157. package/src/adapters/biorxiv/preprints.ts +112 -0
  158. package/src/adapters/cnki/search.ts +357 -0
  159. package/src/adapters/cvf/papers.ts +260 -27
  160. package/src/adapters/dblp/publications.test.ts +9 -0
  161. package/src/adapters/dblp/publications.ts +31 -8
  162. package/src/adapters/google-scholar/search.ts +165 -17
  163. package/src/adapters/hf/paper.test.ts +23 -0
  164. package/src/adapters/hf/paper.ts +89 -5
  165. package/src/adapters/hf/top.yaml +34 -2
  166. package/src/adapters/huggingface-papers/daily.yaml +37 -3
  167. package/src/adapters/huggingface-papers/search.yaml +43 -9
  168. package/src/adapters/medrxiv/preprints.ts +112 -0
  169. package/src/adapters/neurips/proceedings.ts +266 -22
  170. package/src/adapters/openalex/works.test.ts +15 -4
  171. package/src/adapters/openalex/works.ts +136 -8
  172. package/src/adapters/openreview/papers.test.ts +31 -0
  173. package/src/adapters/openreview/papers.ts +407 -29
  174. package/src/adapters/pmlr/proceedings.ts +102 -12
  175. package/src/adapters/pubmed/articles.test.ts +88 -1
  176. package/src/adapters/pubmed/articles.ts +343 -44
  177. package/src/adapters/rxiv/preprints.test.ts +233 -0
  178. package/src/adapters/rxiv/preprints.ts +849 -0
  179. package/src/adapters/scholar-artifacts/pdf-read.ts +277 -0
  180. package/src/adapters/scholar-artifacts/pdf.ts +133 -0
  181. package/src/adapters/semantic-scholar/papers.ts +98 -6
  182. package/src/adapters/unpaywall/works.ts +141 -12
  183. package/src/adapters/wanfang/search.ts +57 -7
  184. package/src/adapters/cnki/search.yaml +0 -49
@@ -1,12 +1,20 @@
1
1
  /**
2
- * @owner src/adapters/openalex/works.ts
3
- * @does Register agent-facing OpenAlex work search and detail commands.
4
- * @needs Public OpenAlex API, TypeScript adapter loader, work id/DOI normalization.
5
- * @feeds surface coverage ledger, scholarly work command surface, agent-readable OpenAlex rows.
6
- * @breaks OpenAlex API envelope drift, weak DOI/id parsing, or abstract reconstruction errors degrade research lookup.
2
+ * @owner src::adapters::openalex::works
3
+ * @does Registers agent-facing OpenAlex work search, detail, and source PDF read commands.
4
+ * @needs Public OpenAlex API, src/adapters/scholar-artifacts/pdf-read.ts, pdftotext
5
+ * @feeds src/commands/scholar.ts via scholar.* capability tags, surface coverage ledger, scholarly work rows
6
+ * @breaks OpenAlex API envelope drift, weak DOI/id parsing, missing OA PDF URLs, or abstract reconstruction errors surface as explicit adapter errors.
7
+ * @invariants Work references normalize to OpenAlex W ids or doi: refs; read requires a source-provided PDF URL before text is claimed.
8
+ * @side-effects HTTPS egress to api.openalex.org and source PDF hosts; read writes one PDF and executes pdftotext.
9
+ * @perf O(limit) JSON mapping for search; O(PDF bytes + extracted page range) for read
10
+ * @concurrency safe
11
+ * @test src/adapters/openalex/works.test.ts, tests/unit/adapters/scholar-sources.test.ts
12
+ * @stability experimental
13
+ * @since 2026-05-19
7
14
  */
8
15
 
9
16
  import { cli, Strategy } from "../../registry.js";
17
+ import { readScholarPdf } from "../scholar-artifacts/pdf-read.js";
10
18
 
11
19
  const OPENALEX_BASE = "https://api.openalex.org";
12
20
  const WORK_ID_RE = /^W\d{4,}$/;
@@ -19,6 +27,7 @@ const SEARCH_SELECT = [
19
27
  "cited_by_count",
20
28
  "authorships",
21
29
  "primary_location",
30
+ "best_oa_location",
22
31
  "open_access",
23
32
  "type",
24
33
  ].join(",");
@@ -31,6 +40,7 @@ const WORK_SELECT = [
31
40
  "cited_by_count",
32
41
  "authorships",
33
42
  "primary_location",
43
+ "best_oa_location",
34
44
  "open_access",
35
45
  "type",
36
46
  "referenced_works",
@@ -51,6 +61,15 @@ interface OpenAlexWork {
51
61
  };
52
62
  }>;
53
63
  primary_location?: {
64
+ landing_page_url?: unknown;
65
+ pdf_url?: unknown;
66
+ source?: {
67
+ display_name?: unknown;
68
+ };
69
+ };
70
+ best_oa_location?: {
71
+ landing_page_url?: unknown;
72
+ pdf_url?: unknown;
54
73
  source?: {
55
74
  display_name?: unknown;
56
75
  };
@@ -173,7 +192,25 @@ function authorList(work: OpenAlexWork): string[] {
173
192
  }
174
193
 
175
194
  function venue(work: OpenAlexWork): string {
176
- return stringField(work.primary_location?.source?.display_name).trim();
195
+ return (
196
+ stringField(work.primary_location?.source?.display_name).trim() ||
197
+ stringField(work.best_oa_location?.source?.display_name).trim()
198
+ );
199
+ }
200
+
201
+ function publisherLandingUrl(work: OpenAlexWork): string {
202
+ return (
203
+ stringField(work.primary_location?.landing_page_url).trim() ||
204
+ stringField(work.best_oa_location?.landing_page_url).trim() ||
205
+ stringField(work.open_access?.oa_url).trim()
206
+ );
207
+ }
208
+
209
+ function openAlexPdfUrl(work: OpenAlexWork): string {
210
+ return (
211
+ stringField(work.primary_location?.pdf_url).trim() ||
212
+ stringField(work.best_oa_location?.pdf_url).trim()
213
+ );
177
214
  }
178
215
 
179
216
  export function mapOpenAlexSearchRows(
@@ -195,7 +232,8 @@ export function mapOpenAlexSearchRows(
195
232
  is_open_access: Boolean(work.open_access?.is_oa),
196
233
  type: stringField(work.type).trim(),
197
234
  doi: bareDoi(work.doi),
198
- pdf_url: stringField(work.open_access?.oa_url).trim(),
235
+ pdf_url: openAlexPdfUrl(work),
236
+ landing_url: publisherLandingUrl(work) || undefined,
199
237
  openalex_id: id,
200
238
  source_adapter: "openalex",
201
239
  source_url: id ? `https://openalex.org/${id}` : "",
@@ -225,7 +263,8 @@ export function mapOpenAlexWorkRow(
225
263
  openAccess: Boolean(work.open_access?.is_oa),
226
264
  is_open_access: Boolean(work.open_access?.is_oa),
227
265
  openAccessUrl: stringField(work.open_access?.oa_url).trim(),
228
- pdf_url: stringField(work.open_access?.oa_url).trim(),
266
+ pdf_url: openAlexPdfUrl(work),
267
+ landing_url: publisherLandingUrl(work) || undefined,
229
268
  referencedCount: Array.isArray(work.referenced_works)
230
269
  ? work.referenced_works.length
231
270
  : null,
@@ -242,6 +281,33 @@ export function mapOpenAlexWorkRow(
242
281
  };
243
282
  }
244
283
 
284
+ async function readOpenAlexWorkPdf(
285
+ row: Record<string, unknown>,
286
+ kwargs: Record<string, unknown>,
287
+ ): Promise<Record<string, unknown>> {
288
+ const pdfUrl = stringField(row.pdf_url);
289
+ if (!pdfUrl) {
290
+ const id = stringField(row.id) || stringField(row.openalex_id) || "record";
291
+ throw new Error(`OpenAlex work ${id} has no source PDF URL.`);
292
+ }
293
+ return readScholarPdf(
294
+ {
295
+ ...kwargs,
296
+ id: stringField(row.id) || stringField(row.openalex_id) || pdfUrl,
297
+ title: stringField(row.title) || pdfUrl,
298
+ source_adapter: "openalex",
299
+ source_url: stringField(row.source_url) || stringField(row.url),
300
+ pdf_url: pdfUrl,
301
+ },
302
+ {
303
+ site: "openalex",
304
+ command: "read",
305
+ defaultOutput: "./openalex-downloads",
306
+ userAgent: "unicli-openalex/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
307
+ },
308
+ );
309
+ }
310
+
245
311
  async function fetchOpenAlex(url: string, label: string): Promise<unknown> {
246
312
  const response = await fetch(appendMailto(url), {
247
313
  headers: {
@@ -350,3 +416,65 @@ cli({
350
416
  return [mapOpenAlexWorkRow(work)];
351
417
  },
352
418
  });
419
+
420
+ cli({
421
+ site: "openalex",
422
+ name: "read",
423
+ description: "Download an OpenAlex open-access Work PDF and extract text",
424
+ domain: "api.openalex.org",
425
+ strategy: Strategy.PUBLIC,
426
+ args: [
427
+ {
428
+ name: "id",
429
+ type: "str",
430
+ required: true,
431
+ positional: true,
432
+ description: "OpenAlex Work id or DOI",
433
+ },
434
+ {
435
+ name: "output",
436
+ type: "str",
437
+ default: "./openalex-downloads",
438
+ description: "Output directory for the downloaded PDF",
439
+ "x-unicli-kind": "path",
440
+ },
441
+ { name: "filename", type: "str", description: "Output PDF filename" },
442
+ { name: "first-page", type: "int", default: 1, description: "First page" },
443
+ { name: "last-page", type: "int", default: 20, description: "Last page" },
444
+ {
445
+ name: "max-chars",
446
+ type: "int",
447
+ default: 40000,
448
+ description: "Maximum extracted text characters",
449
+ },
450
+ ],
451
+ columns: [
452
+ "id",
453
+ "title",
454
+ "source_adapter",
455
+ "source_url",
456
+ "pdf_url",
457
+ "path",
458
+ "text_source",
459
+ "text",
460
+ "text_chars",
461
+ "text_truncated",
462
+ ],
463
+ capabilities: [
464
+ "http.fetch",
465
+ "http.download",
466
+ "subprocess.exec",
467
+ "scholar.fulltext",
468
+ "scholar.pdf",
469
+ ],
470
+ executables: ["pdftotext"],
471
+ minimum_capability: "subprocess.exec",
472
+ func: async (_page, kwargs) => {
473
+ const ref = requireOpenAlexWorkRef(kwargs.id);
474
+ const work = (await fetchOpenAlex(
475
+ `${OPENALEX_BASE}/works/${encodeURIComponent(ref)}?select=${WORK_SELECT}`,
476
+ "openalex work",
477
+ )) as OpenAlexWork;
478
+ return [await readOpenAlexWorkPdf(mapOpenAlexWorkRow(work), kwargs)];
479
+ },
480
+ });
@@ -7,10 +7,13 @@ import {
7
7
  formatOpenReviewDate,
8
8
  mapOpenReviewNoteRow,
9
9
  mapReviewThreadRows,
10
+ openReviewPdfFilename,
10
11
  readContent,
11
12
  requireForumId,
13
+ requireOpenReviewMaxChars,
12
14
  requireOpenReviewLimit,
13
15
  requireOpenReviewOffset,
16
+ requireOpenReviewPageRange,
14
17
  requireProfileId,
15
18
  } from "./papers.js";
16
19
 
@@ -26,11 +29,25 @@ describe("openreview agent-facing paper commands", () => {
26
29
  expect(requireOpenReviewOffset(undefined)).toBe(0);
27
30
  expect(() => requireOpenReviewOffset("-1")).toThrow("non-negative");
28
31
  expect(requireForumId("5sRnsubyAK")).toBe("5sRnsubyAK");
32
+ expect(requireForumId("https://openreview.net/forum?id=5sRnsubyAK")).toBe(
33
+ "5sRnsubyAK",
34
+ );
29
35
  expect(() => requireForumId("https://openreview.net/forum?id=x")).toThrow(
30
36
  "not a valid",
31
37
  );
32
38
  expect(requireProfileId("~Yoshua_Bengio1")).toBe("~Yoshua_Bengio1");
33
39
  expect(() => requireProfileId("Yoshua_Bengio1")).toThrow("not valid");
40
+ expect(requireOpenReviewPageRange("2", "4")).toEqual({
41
+ firstPage: 2,
42
+ lastPage: 4,
43
+ });
44
+ expect(() => requireOpenReviewPageRange("4", "2")).toThrow("last-page");
45
+ expect(requireOpenReviewMaxChars(undefined)).toBe(40000);
46
+ expect(requireOpenReviewMaxChars("1000")).toBe(1000);
47
+ expect(() => requireOpenReviewMaxChars("999")).toThrow("max-chars");
48
+ expect(openReviewPdfFilename("abcDEF123", "A / Paper: Demo")).toBe(
49
+ "abcDEF123-A-Paper-Demo.pdf",
50
+ );
34
51
  });
35
52
 
36
53
  it("maps OpenReview content.value note fields", () => {
@@ -64,7 +81,14 @@ describe("openreview agent-facing paper commands", () => {
64
81
  abstract: "Long abstract",
65
82
  pdate: "2025-10-09",
66
83
  pdf: "https://openreview.net/pdf/abc.pdf",
84
+ pdf_url: "https://openreview.net/pdf/abc.pdf",
67
85
  url: "https://openreview.net/forum?id=5sRnsubyAK",
86
+ source_url: "https://openreview.net/forum?id=5sRnsubyAK",
87
+ landing_url: "https://openreview.net/forum?id=5sRnsubyAK",
88
+ source_adapter: "openreview",
89
+ openreview_id: "5sRnsubyAK",
90
+ date: "2025-10-09",
91
+ retrieved_at: expect.any(String),
68
92
  });
69
93
  });
70
94
 
@@ -129,9 +153,16 @@ describe("openreview agent-facing paper commands", () => {
129
153
  "DECISION",
130
154
  ]);
131
155
  expect(rows[1]).toMatchObject({
156
+ forum: "forum123",
157
+ note_id: "early",
132
158
  author: "Reviewer_abc",
159
+ invitation: "Venue/-/Official_Review",
160
+ created_at: "1970-01-01",
161
+ source_url: "https://openreview.net/forum?id=forum123&noteId=early",
133
162
  rating: "8: accept",
134
163
  confidence: "4: high",
164
+ text_chars: expect.any(Number),
165
+ text_truncated: true,
135
166
  });
136
167
  expect(String(rows[1].text)).toHaveLength(200);
137
168
  expect(String(rows[1].text).endsWith("...")).toBe(true);