@struktur/sdk 2.1.2 → 2.3.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.
Files changed (200) hide show
  1. package/dist/artifacts/fileToArtifact.d.ts +8 -0
  2. package/dist/artifacts/fileToArtifact.d.ts.map +1 -0
  3. package/dist/artifacts/input.d.ts +60 -0
  4. package/dist/artifacts/input.d.ts.map +1 -0
  5. package/{src/artifacts/providers.ts → dist/artifacts/providers.d.ts} +2 -4
  6. package/dist/artifacts/providers.d.ts.map +1 -0
  7. package/dist/artifacts/urlToArtifact.d.ts +3 -0
  8. package/dist/artifacts/urlToArtifact.d.ts.map +1 -0
  9. package/dist/auth/config.d.ts +34 -0
  10. package/dist/auth/config.d.ts.map +1 -0
  11. package/dist/auth/tokens.d.ts +18 -0
  12. package/dist/auth/tokens.d.ts.map +1 -0
  13. package/dist/chunking/ArtifactBatcher.d.ts +11 -0
  14. package/dist/chunking/ArtifactBatcher.d.ts.map +1 -0
  15. package/dist/chunking/ArtifactSplitter.d.ts +10 -0
  16. package/dist/chunking/ArtifactSplitter.d.ts.map +1 -0
  17. package/dist/debug/logger.d.ts +169 -0
  18. package/dist/debug/logger.d.ts.map +1 -0
  19. package/dist/extract.d.ts +3 -0
  20. package/dist/extract.d.ts.map +1 -0
  21. package/dist/fields.d.ts +75 -0
  22. package/dist/fields.d.ts.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +5603 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/llm/LLMClient.d.ts +40 -0
  28. package/dist/llm/LLMClient.d.ts.map +1 -0
  29. package/dist/llm/RetryingRunner.d.ts +37 -0
  30. package/dist/llm/RetryingRunner.d.ts.map +1 -0
  31. package/dist/llm/message.d.ts +12 -0
  32. package/dist/llm/message.d.ts.map +1 -0
  33. package/dist/llm/models.d.ts +13 -0
  34. package/dist/llm/models.d.ts.map +1 -0
  35. package/dist/llm/resolveModel.d.ts +3 -0
  36. package/dist/llm/resolveModel.d.ts.map +1 -0
  37. package/dist/merge/Deduplicator.d.ts +4 -0
  38. package/dist/merge/Deduplicator.d.ts.map +1 -0
  39. package/dist/merge/SmartDataMerger.d.ts +7 -0
  40. package/dist/merge/SmartDataMerger.d.ts.map +1 -0
  41. package/dist/parsers/collect.d.ts +7 -0
  42. package/dist/parsers/collect.d.ts.map +1 -0
  43. package/{src/parsers/index.ts → dist/parsers/index.d.ts} +1 -0
  44. package/dist/parsers/index.d.ts.map +1 -0
  45. package/dist/parsers/mime.d.ts +12 -0
  46. package/dist/parsers/mime.d.ts.map +1 -0
  47. package/dist/parsers/npm.d.ts +16 -0
  48. package/dist/parsers/npm.d.ts.map +1 -0
  49. package/dist/parsers/pdf.d.ts +36 -0
  50. package/dist/parsers/pdf.d.ts.map +1 -0
  51. package/dist/parsers/runner.d.ts +4 -0
  52. package/dist/parsers/runner.d.ts.map +1 -0
  53. package/dist/parsers/types.d.ts +27 -0
  54. package/dist/parsers/types.d.ts.map +1 -0
  55. package/dist/parsers.d.ts +1 -0
  56. package/dist/parsers.js +492 -0
  57. package/dist/parsers.js.map +1 -0
  58. package/dist/prompts/DeduplicationPrompt.d.ts +5 -0
  59. package/dist/prompts/DeduplicationPrompt.d.ts.map +1 -0
  60. package/dist/prompts/ExtractorPrompt.d.ts +6 -0
  61. package/dist/prompts/ExtractorPrompt.d.ts.map +1 -0
  62. package/dist/prompts/ParallelMergerPrompt.d.ts +5 -0
  63. package/dist/prompts/ParallelMergerPrompt.d.ts.map +1 -0
  64. package/dist/prompts/SequentialExtractorPrompt.d.ts +6 -0
  65. package/dist/prompts/SequentialExtractorPrompt.d.ts.map +1 -0
  66. package/dist/prompts/formatArtifacts.d.ts +3 -0
  67. package/dist/prompts/formatArtifacts.d.ts.map +1 -0
  68. package/dist/strategies/DoublePassAutoMergeStrategy.d.ts +23 -0
  69. package/dist/strategies/DoublePassAutoMergeStrategy.d.ts.map +1 -0
  70. package/dist/strategies/DoublePassStrategy.d.ts +22 -0
  71. package/dist/strategies/DoublePassStrategy.d.ts.map +1 -0
  72. package/dist/strategies/ParallelAutoMergeStrategy.d.ts +27 -0
  73. package/dist/strategies/ParallelAutoMergeStrategy.d.ts.map +1 -0
  74. package/dist/strategies/ParallelStrategy.d.ts +22 -0
  75. package/dist/strategies/ParallelStrategy.d.ts.map +1 -0
  76. package/dist/strategies/SequentialAutoMergeStrategy.d.ts +22 -0
  77. package/dist/strategies/SequentialAutoMergeStrategy.d.ts.map +1 -0
  78. package/dist/strategies/SequentialStrategy.d.ts +20 -0
  79. package/dist/strategies/SequentialStrategy.d.ts.map +1 -0
  80. package/dist/strategies/SimpleStrategy.d.ts +18 -0
  81. package/dist/strategies/SimpleStrategy.d.ts.map +1 -0
  82. package/dist/strategies/agent/AgentStrategy.d.ts +44 -0
  83. package/dist/strategies/agent/AgentStrategy.d.ts.map +1 -0
  84. package/dist/strategies/agent/AgentTools.d.ts +55 -0
  85. package/dist/strategies/agent/AgentTools.d.ts.map +1 -0
  86. package/dist/strategies/agent/ArtifactFilesystem.d.ts +51 -0
  87. package/dist/strategies/agent/ArtifactFilesystem.d.ts.map +1 -0
  88. package/dist/strategies/agent/index.d.ts +4 -0
  89. package/dist/strategies/agent/index.d.ts.map +1 -0
  90. package/dist/strategies/concurrency.d.ts +2 -0
  91. package/dist/strategies/concurrency.d.ts.map +1 -0
  92. package/{src/strategies/index.ts → dist/strategies/index.d.ts} +2 -0
  93. package/dist/strategies/index.d.ts.map +1 -0
  94. package/dist/strategies/utils.d.ts +39 -0
  95. package/dist/strategies/utils.d.ts.map +1 -0
  96. package/dist/strategies.d.ts +1 -0
  97. package/dist/strategies.js +3930 -0
  98. package/dist/strategies.js.map +1 -0
  99. package/dist/tokenization.d.ts +11 -0
  100. package/dist/tokenization.d.ts.map +1 -0
  101. package/dist/types.d.ts +178 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/validation/validator.d.ts +20 -0
  104. package/dist/validation/validator.d.ts.map +1 -0
  105. package/package.json +30 -14
  106. package/src/agent-cli-integration.test.ts +0 -47
  107. package/src/agent-export.test.ts +0 -17
  108. package/src/agent-tool-labels.test.ts +0 -50
  109. package/src/artifacts/AGENTS.md +0 -16
  110. package/src/artifacts/fileToArtifact.test.ts +0 -37
  111. package/src/artifacts/fileToArtifact.ts +0 -44
  112. package/src/artifacts/input.test.ts +0 -243
  113. package/src/artifacts/input.ts +0 -360
  114. package/src/artifacts/providers.test.ts +0 -19
  115. package/src/artifacts/urlToArtifact.test.ts +0 -23
  116. package/src/artifacts/urlToArtifact.ts +0 -19
  117. package/src/auth/AGENTS.md +0 -11
  118. package/src/auth/config.test.ts +0 -132
  119. package/src/auth/config.ts +0 -186
  120. package/src/auth/tokens.test.ts +0 -58
  121. package/src/auth/tokens.ts +0 -229
  122. package/src/chunking/AGENTS.md +0 -11
  123. package/src/chunking/ArtifactBatcher.test.ts +0 -22
  124. package/src/chunking/ArtifactBatcher.ts +0 -110
  125. package/src/chunking/ArtifactSplitter.test.ts +0 -38
  126. package/src/chunking/ArtifactSplitter.ts +0 -151
  127. package/src/debug/AGENTS.md +0 -79
  128. package/src/debug/logger.test.ts +0 -244
  129. package/src/debug/logger.ts +0 -211
  130. package/src/extract.test.ts +0 -22
  131. package/src/extract.ts +0 -150
  132. package/src/fields.test.ts +0 -681
  133. package/src/fields.ts +0 -246
  134. package/src/index.test.ts +0 -20
  135. package/src/index.ts +0 -110
  136. package/src/llm/AGENTS.md +0 -9
  137. package/src/llm/LLMClient.test.ts +0 -394
  138. package/src/llm/LLMClient.ts +0 -264
  139. package/src/llm/RetryingRunner.test.ts +0 -174
  140. package/src/llm/RetryingRunner.ts +0 -270
  141. package/src/llm/message.test.ts +0 -42
  142. package/src/llm/message.ts +0 -47
  143. package/src/llm/models.test.ts +0 -82
  144. package/src/llm/models.ts +0 -190
  145. package/src/llm/resolveModel.ts +0 -86
  146. package/src/merge/AGENTS.md +0 -6
  147. package/src/merge/Deduplicator.test.ts +0 -108
  148. package/src/merge/Deduplicator.ts +0 -45
  149. package/src/merge/SmartDataMerger.test.ts +0 -177
  150. package/src/merge/SmartDataMerger.ts +0 -56
  151. package/src/parsers/AGENTS.md +0 -58
  152. package/src/parsers/collect.test.ts +0 -56
  153. package/src/parsers/collect.ts +0 -31
  154. package/src/parsers/mime.test.ts +0 -91
  155. package/src/parsers/mime.ts +0 -137
  156. package/src/parsers/npm.ts +0 -26
  157. package/src/parsers/pdf.test.ts +0 -394
  158. package/src/parsers/pdf.ts +0 -194
  159. package/src/parsers/runner.test.ts +0 -95
  160. package/src/parsers/runner.ts +0 -177
  161. package/src/parsers/types.ts +0 -29
  162. package/src/prompts/AGENTS.md +0 -8
  163. package/src/prompts/DeduplicationPrompt.test.ts +0 -41
  164. package/src/prompts/DeduplicationPrompt.ts +0 -37
  165. package/src/prompts/ExtractorPrompt.test.ts +0 -21
  166. package/src/prompts/ExtractorPrompt.ts +0 -72
  167. package/src/prompts/ParallelMergerPrompt.test.ts +0 -8
  168. package/src/prompts/ParallelMergerPrompt.ts +0 -37
  169. package/src/prompts/SequentialExtractorPrompt.test.ts +0 -24
  170. package/src/prompts/SequentialExtractorPrompt.ts +0 -82
  171. package/src/prompts/formatArtifacts.test.ts +0 -39
  172. package/src/prompts/formatArtifacts.ts +0 -46
  173. package/src/strategies/AGENTS.md +0 -6
  174. package/src/strategies/DoublePassAutoMergeStrategy.test.ts +0 -53
  175. package/src/strategies/DoublePassAutoMergeStrategy.ts +0 -410
  176. package/src/strategies/DoublePassStrategy.test.ts +0 -48
  177. package/src/strategies/DoublePassStrategy.ts +0 -266
  178. package/src/strategies/ParallelAutoMergeStrategy.test.ts +0 -152
  179. package/src/strategies/ParallelAutoMergeStrategy.ts +0 -345
  180. package/src/strategies/ParallelStrategy.test.ts +0 -61
  181. package/src/strategies/ParallelStrategy.ts +0 -208
  182. package/src/strategies/SequentialAutoMergeStrategy.test.ts +0 -66
  183. package/src/strategies/SequentialAutoMergeStrategy.ts +0 -325
  184. package/src/strategies/SequentialStrategy.test.ts +0 -53
  185. package/src/strategies/SequentialStrategy.ts +0 -142
  186. package/src/strategies/SimpleStrategy.test.ts +0 -46
  187. package/src/strategies/SimpleStrategy.ts +0 -94
  188. package/src/strategies/concurrency.test.ts +0 -16
  189. package/src/strategies/concurrency.ts +0 -14
  190. package/src/strategies/index.test.ts +0 -20
  191. package/src/strategies/utils.test.ts +0 -76
  192. package/src/strategies/utils.ts +0 -95
  193. package/src/tokenization.test.ts +0 -119
  194. package/src/tokenization.ts +0 -71
  195. package/src/types.test.ts +0 -25
  196. package/src/types.ts +0 -174
  197. package/src/validation/AGENTS.md +0 -7
  198. package/src/validation/validator.test.ts +0 -204
  199. package/src/validation/validator.ts +0 -90
  200. package/tsconfig.json +0 -22
@@ -1,681 +0,0 @@
1
- import { test, expect, describe } from "bun:test";
2
- import {
3
- parseFieldsString,
4
- buildSchemaFromParsedFields,
5
- buildSchemaFromFields,
6
- } from "./fields";
7
-
8
- // ---------------------------------------------------------------------------
9
- // parseFieldsString — scalars (positive)
10
- // ---------------------------------------------------------------------------
11
-
12
- describe("parseFieldsString — scalars", () => {
13
- test("single field defaults to string", () => {
14
- expect(parseFieldsString("title")).toEqual([
15
- { name: "title", kind: "scalar", type: "string" },
16
- ]);
17
- });
18
-
19
- test("two fields without types", () => {
20
- expect(parseFieldsString("title,description")).toEqual([
21
- { name: "title", kind: "scalar", type: "string" },
22
- { name: "description", kind: "scalar", type: "string" },
23
- ]);
24
- });
25
-
26
- test("explicit :string is same as omitting type", () => {
27
- expect(parseFieldsString("title:string")).toEqual([
28
- { name: "title", kind: "scalar", type: "string" },
29
- ]);
30
- });
31
-
32
- test("spaces around comma — no types", () => {
33
- expect(parseFieldsString("title, description")).toEqual([
34
- { name: "title", kind: "scalar", type: "string" },
35
- { name: "description", kind: "scalar", type: "string" },
36
- ]);
37
- });
38
-
39
- test("spaces around comma and colon", () => {
40
- expect(parseFieldsString("title , price: number")).toEqual([
41
- { name: "title", kind: "scalar", type: "string" },
42
- { name: "price", kind: "scalar", type: "number" },
43
- ]);
44
- });
45
-
46
- test("spaces around colon only", () => {
47
- expect(parseFieldsString("title, price:number")).toEqual([
48
- { name: "title", kind: "scalar", type: "string" },
49
- { name: "price", kind: "scalar", type: "number" },
50
- ]);
51
- });
52
-
53
- test("all four scalar types", () => {
54
- expect(parseFieldsString("a:string,b:number,c:boolean,d:integer")).toEqual([
55
- { name: "a", kind: "scalar", type: "string" },
56
- { name: "b", kind: "scalar", type: "number" },
57
- { name: "c", kind: "scalar", type: "boolean" },
58
- { name: "d", kind: "scalar", type: "integer" },
59
- ]);
60
- });
61
-
62
- test("bool is an alias for boolean", () => {
63
- expect(parseFieldsString("active:bool")).toEqual([
64
- { name: "active", kind: "scalar", type: "boolean" },
65
- ]);
66
- });
67
-
68
- test("float is an alias for number", () => {
69
- expect(parseFieldsString("ratio:float")).toEqual([
70
- { name: "ratio", kind: "scalar", type: "number" },
71
- ]);
72
- });
73
-
74
- test("int resolves to int (triggers multipleOf:1 in schema)", () => {
75
- expect(parseFieldsString("count:int")).toEqual([
76
- { name: "count", kind: "scalar", type: "int" },
77
- ]);
78
- });
79
-
80
- test("all aliases together", () => {
81
- expect(parseFieldsString("n:int,r:float,f:bool")).toEqual([
82
- { name: "n", kind: "scalar", type: "int" },
83
- { name: "r", kind: "scalar", type: "number" },
84
- { name: "f", kind: "scalar", type: "boolean" },
85
- ]);
86
- });
87
-
88
- test("many fields, all untyped", () => {
89
- expect(parseFieldsString("id,name,email,phone,address")).toEqual([
90
- { name: "id", kind: "scalar", type: "string" },
91
- { name: "name", kind: "scalar", type: "string" },
92
- { name: "email", kind: "scalar", type: "string" },
93
- { name: "phone", kind: "scalar", type: "string" },
94
- { name: "address", kind: "scalar", type: "string" },
95
- ]);
96
- });
97
-
98
- test("leading/trailing whitespace in entire string is ignored", () => {
99
- expect(parseFieldsString(" title ")).toEqual([
100
- { name: "title", kind: "scalar", type: "string" },
101
- ]);
102
- });
103
- });
104
-
105
- // ---------------------------------------------------------------------------
106
- // parseFieldsString — scalars (error cases)
107
- // ---------------------------------------------------------------------------
108
-
109
- describe("parseFieldsString — scalar errors", () => {
110
- test("empty string throws with helpful message", () => {
111
- expect(() => parseFieldsString("")).toThrow("Fields string must not be empty.");
112
- });
113
-
114
- test("whitespace-only string throws", () => {
115
- expect(() => parseFieldsString(" ")).toThrow("Fields string must not be empty.");
116
- });
117
-
118
- test("unknown scalar type names the offending type and field", () => {
119
- const err = () => parseFieldsString("title:object");
120
- expect(err).toThrow(/Unknown type "object"/);
121
- expect(err).toThrow(/field "title"/);
122
- expect(err).toThrow(/bool, boolean, float, int, integer, number, string/);
123
- });
124
-
125
- test("unknown type also hints at complex types", () => {
126
- expect(() => parseFieldsString("x:map")).toThrow(/enum\{/);
127
- expect(() => parseFieldsString("x:map")).toThrow(/array\{/);
128
- });
129
-
130
- test("leading comma — empty field name before it", () => {
131
- expect(() => parseFieldsString(",foo")).toThrow(/Empty field name/);
132
- });
133
-
134
- test("trailing comma is silently ignored", () => {
135
- // The brace-depth splitter drops empty trailing tokens — same as most CLIs.
136
- expect(parseFieldsString("foo,")).toEqual([
137
- { name: "foo", kind: "scalar", type: "string" },
138
- ]);
139
- });
140
-
141
- test("consecutive commas — empty field name between them", () => {
142
- expect(() => parseFieldsString("foo,,bar")).toThrow(/Empty field name/);
143
- });
144
-
145
- test("colon with no type mentions the field name", () => {
146
- const err = () => parseFieldsString("title:");
147
- expect(err).toThrow(/Empty type/);
148
- expect(err).toThrow(/"title"/);
149
- });
150
-
151
- test("colon with whitespace-only type is treated as empty", () => {
152
- expect(() => parseFieldsString("title: ")).toThrow(/Empty type/);
153
- });
154
-
155
- test("field name with only a colon and no name", () => {
156
- expect(() => parseFieldsString(":string")).toThrow(/Empty field name/);
157
- });
158
- });
159
-
160
- // ---------------------------------------------------------------------------
161
- // parseFieldsString — enums (positive)
162
- // ---------------------------------------------------------------------------
163
-
164
- describe("parseFieldsString — enums", () => {
165
- test("two-value enum", () => {
166
- expect(parseFieldsString("status:enum{draft|published}")).toEqual([
167
- { name: "status", kind: "enum", values: ["draft", "published"] },
168
- ]);
169
- });
170
-
171
- test("three-value enum with numbers (user example)", () => {
172
- expect(parseFieldsString("wtf:enum{abc|def|123}")).toEqual([
173
- { name: "wtf", kind: "enum", values: ["abc", "def", "123"] },
174
- ]);
175
- });
176
-
177
- test("spaces around pipe values are trimmed", () => {
178
- expect(parseFieldsString("status:enum{ draft | published | archived }")).toEqual([
179
- { name: "status", kind: "enum", values: ["draft", "published", "archived"] },
180
- ]);
181
- });
182
-
183
- test("enum field preceded by plain field", () => {
184
- expect(parseFieldsString("name, status:enum{active|inactive}")).toEqual([
185
- { name: "name", kind: "scalar", type: "string" },
186
- { name: "status", kind: "enum", values: ["active", "inactive"] },
187
- ]);
188
- });
189
-
190
- test("enum field followed by plain field", () => {
191
- expect(parseFieldsString("status:enum{a|b}, title")).toEqual([
192
- { name: "status", kind: "enum", values: ["a", "b"] },
193
- { name: "title", kind: "scalar", type: "string" },
194
- ]);
195
- });
196
-
197
- test("enum sandwiched between other fields", () => {
198
- expect(parseFieldsString("id, role:enum{admin|user|guest}, name")).toEqual([
199
- { name: "id", kind: "scalar", type: "string" },
200
- { name: "role", kind: "enum", values: ["admin", "user", "guest"] },
201
- { name: "name", kind: "scalar", type: "string" },
202
- ]);
203
- });
204
-
205
- test("enum values that look like numbers", () => {
206
- expect(parseFieldsString("code:enum{1|2|3}")).toEqual([
207
- { name: "code", kind: "enum", values: ["1", "2", "3"] },
208
- ]);
209
- });
210
-
211
- test("enum values with hyphens and underscores", () => {
212
- expect(parseFieldsString("type:enum{in-progress|not_started|done}")).toEqual([
213
- { name: "type", kind: "enum", values: ["in-progress", "not_started", "done"] },
214
- ]);
215
- });
216
- });
217
-
218
- // ---------------------------------------------------------------------------
219
- // parseFieldsString — enums (error cases)
220
- // ---------------------------------------------------------------------------
221
-
222
- describe("parseFieldsString — enum errors", () => {
223
- test("single value enum throws and mentions field name", () => {
224
- const err = () => parseFieldsString("x:enum{only}");
225
- expect(err).toThrow(/at least two/);
226
- expect(err).toThrow(/"x"/);
227
- });
228
-
229
- test("empty braces throws", () => {
230
- const err = () => parseFieldsString("x:enum{}");
231
- expect(err).toThrow(/at least two/);
232
- });
233
-
234
- test("braces with only whitespace throws", () => {
235
- expect(() => parseFieldsString("x:enum{ }")).toThrow(/at least two/);
236
- });
237
-
238
- test("enum with only pipe separators and no values throws", () => {
239
- expect(() => parseFieldsString("x:enum{|}")).toThrow(/at least two/);
240
- });
241
-
242
- test("missing closing brace gives Unmatched braces error", () => {
243
- expect(() => parseFieldsString("x:enum{a|b")).toThrow(/Unmatched braces/);
244
- });
245
- });
246
-
247
- // ---------------------------------------------------------------------------
248
- // parseFieldsString — arrays (positive)
249
- // ---------------------------------------------------------------------------
250
-
251
- describe("parseFieldsString — arrays", () => {
252
- test("array of string", () => {
253
- expect(parseFieldsString("tags:array{string}")).toEqual([
254
- { name: "tags", kind: "array", items: "string" },
255
- ]);
256
- });
257
-
258
- test("array shorthand defaults to string", () => {
259
- expect(parseFieldsString("tags:array")).toEqual([
260
- { name: "tags", kind: "array", items: "string" },
261
- ]);
262
- });
263
-
264
- test("array of number", () => {
265
- expect(parseFieldsString("scores:array{number}")).toEqual([
266
- { name: "scores", kind: "array", items: "number" },
267
- ]);
268
- });
269
-
270
- test("array of boolean", () => {
271
- expect(parseFieldsString("flags:array{boolean}")).toEqual([
272
- { name: "flags", kind: "array", items: "boolean" },
273
- ]);
274
- });
275
-
276
- test("array of integer", () => {
277
- expect(parseFieldsString("ids:array{integer}")).toEqual([
278
- { name: "ids", kind: "array", items: "integer" },
279
- ]);
280
- });
281
-
282
- test("array of int", () => {
283
- expect(parseFieldsString("ids:array{int}")).toEqual([
284
- { name: "ids", kind: "array", items: "int" },
285
- ]);
286
- });
287
-
288
- test("array of bool", () => {
289
- expect(parseFieldsString("flags:array{bool}")).toEqual([
290
- { name: "flags", kind: "array", items: "boolean" },
291
- ]);
292
- });
293
-
294
- test("array of float", () => {
295
- expect(parseFieldsString("scores:array{float}")).toEqual([
296
- { name: "scores", kind: "array", items: "number" },
297
- ]);
298
- });
299
-
300
- test("array preceded by plain field (user example)", () => {
301
- expect(parseFieldsString("name, addresses:array{string}")).toEqual([
302
- { name: "name", kind: "scalar", type: "string" },
303
- { name: "addresses", kind: "array", items: "string" },
304
- ]);
305
- });
306
-
307
- test("array followed by plain field", () => {
308
- expect(parseFieldsString("tags:array{string}, title")).toEqual([
309
- { name: "tags", kind: "array", items: "string" },
310
- { name: "title", kind: "scalar", type: "string" },
311
- ]);
312
- });
313
-
314
- test("whitespace inside braces is trimmed", () => {
315
- expect(parseFieldsString("tags:array{ string }")).toEqual([
316
- { name: "tags", kind: "array", items: "string" },
317
- ]);
318
- });
319
- });
320
-
321
- // ---------------------------------------------------------------------------
322
- // parseFieldsString — arrays (error cases)
323
- // ---------------------------------------------------------------------------
324
-
325
- describe("parseFieldsString — array errors", () => {
326
- test("unknown item type names the offending type and field", () => {
327
- const err = () => parseFieldsString("x:array{object}");
328
- expect(err).toThrow(/Unknown type "object"/);
329
- expect(err).toThrow(/field "x"/);
330
- });
331
-
332
- test("empty braces throws with field name", () => {
333
- const err = () => parseFieldsString("x:array{}");
334
- expect(err).toThrow(/requires an item type/);
335
- expect(err).toThrow(/"x"/);
336
- });
337
-
338
- test("whitespace-only braces throws", () => {
339
- expect(() => parseFieldsString("x:array{ }")).toThrow(/requires an item type/);
340
- });
341
-
342
- test("missing closing brace gives Unmatched braces error", () => {
343
- expect(() => parseFieldsString("x:array{string")).toThrow(/Unmatched braces/);
344
- });
345
- });
346
-
347
- // ---------------------------------------------------------------------------
348
- // parseFieldsString — brace depth / structural errors
349
- // ---------------------------------------------------------------------------
350
-
351
- describe("parseFieldsString — structural errors", () => {
352
- test("unmatched open brace throws", () => {
353
- expect(() => parseFieldsString("x:array{string, y")).toThrow(/Unmatched braces/);
354
- });
355
- });
356
-
357
- // ---------------------------------------------------------------------------
358
- // buildSchemaFromParsedFields
359
- // ---------------------------------------------------------------------------
360
-
361
- describe("buildSchemaFromParsedFields", () => {
362
- test("single string field", () => {
363
- expect(buildSchemaFromParsedFields([
364
- { name: "title", kind: "scalar", type: "string" },
365
- ])).toEqual({
366
- type: "object",
367
- properties: { title: { type: "string" } },
368
- required: ["title"],
369
- additionalProperties: false,
370
- });
371
- });
372
-
373
- test("two scalar fields", () => {
374
- expect(buildSchemaFromParsedFields([
375
- { name: "title", kind: "scalar", type: "string" },
376
- { name: "price", kind: "scalar", type: "number" },
377
- ])).toEqual({
378
- type: "object",
379
- properties: {
380
- title: { type: "string" },
381
- price: { type: "number" },
382
- },
383
- required: ["title", "price"],
384
- additionalProperties: false,
385
- });
386
- });
387
-
388
- test("enum field produces string type with enum array", () => {
389
- expect(buildSchemaFromParsedFields([
390
- { name: "status", kind: "enum", values: ["draft", "published"] },
391
- ])).toEqual({
392
- type: "object",
393
- properties: {
394
- status: { type: "string", enum: ["draft", "published"] },
395
- },
396
- required: ["status"],
397
- additionalProperties: false,
398
- });
399
- });
400
-
401
- test("array field produces array type with items", () => {
402
- expect(buildSchemaFromParsedFields([
403
- { name: "tags", kind: "array", items: "string" },
404
- ])).toEqual({
405
- type: "object",
406
- properties: {
407
- tags: { type: "array", items: { type: "string" } },
408
- },
409
- required: ["tags"],
410
- additionalProperties: false,
411
- });
412
- });
413
-
414
- test("all three kinds together", () => {
415
- expect(buildSchemaFromParsedFields([
416
- { name: "title", kind: "scalar", type: "string" },
417
- { name: "status", kind: "enum", values: ["a", "b"] },
418
- { name: "tags", kind: "array", items: "string" },
419
- ])).toEqual({
420
- type: "object",
421
- properties: {
422
- title: { type: "string" },
423
- status: { type: "string", enum: ["a", "b"] },
424
- tags: { type: "array", items: { type: "string" } },
425
- },
426
- required: ["title", "status", "tags"],
427
- additionalProperties: false,
428
- });
429
- });
430
-
431
- test("int field produces integer type with multipleOf:1", () => {
432
- expect(buildSchemaFromParsedFields([
433
- { name: "count", kind: "scalar", type: "int" },
434
- ])).toEqual({
435
- type: "object",
436
- properties: { count: { type: "integer", multipleOf: 1 } },
437
- required: ["count"],
438
- additionalProperties: false,
439
- });
440
- });
441
-
442
- test("float field produces plain number type (no multipleOf)", () => {
443
- expect(buildSchemaFromParsedFields([
444
- { name: "ratio", kind: "scalar", type: "number" },
445
- ])).toEqual({
446
- type: "object",
447
- properties: { ratio: { type: "number" } },
448
- required: ["ratio"],
449
- additionalProperties: false,
450
- });
451
- });
452
-
453
- test("array of int items produces integer items with multipleOf:1", () => {
454
- expect(buildSchemaFromParsedFields([
455
- { name: "ids", kind: "array", items: "int" },
456
- ])).toEqual({
457
- type: "object",
458
- properties: { ids: { type: "array", items: { type: "integer", multipleOf: 1 } } },
459
- required: ["ids"],
460
- additionalProperties: false,
461
- });
462
- });
463
- test("throws on empty array with helpful message", () => {
464
- expect(() => buildSchemaFromParsedFields([])).toThrow(
465
- "Cannot build a schema from an empty fields list.",
466
- );
467
- });
468
- });
469
-
470
- // ---------------------------------------------------------------------------
471
- // buildSchemaFromFields (end-to-end convenience wrapper)
472
- // ---------------------------------------------------------------------------
473
-
474
- describe("buildSchemaFromFields", () => {
475
- test("single untyped field", () => {
476
- expect(buildSchemaFromFields("title")).toEqual({
477
- type: "object",
478
- properties: { title: { type: "string" } },
479
- required: ["title"],
480
- additionalProperties: false,
481
- });
482
- });
483
-
484
- test("two untyped fields", () => {
485
- expect(buildSchemaFromFields("title,description")).toEqual({
486
- type: "object",
487
- properties: {
488
- title: { type: "string" },
489
- description: { type: "string" },
490
- },
491
- required: ["title", "description"],
492
- additionalProperties: false,
493
- });
494
- });
495
-
496
- test("mixed scalar types", () => {
497
- expect(buildSchemaFromFields("title, price:number")).toEqual({
498
- type: "object",
499
- properties: {
500
- title: { type: "string" },
501
- price: { type: "number" },
502
- },
503
- required: ["title", "price"],
504
- additionalProperties: false,
505
- });
506
- });
507
-
508
- test("enum (user example: name,wtf:enum{abc|def|123})", () => {
509
- expect(buildSchemaFromFields("name,wtf:enum{abc|def|123}")).toEqual({
510
- type: "object",
511
- properties: {
512
- name: { type: "string" },
513
- wtf: { type: "string", enum: ["abc", "def", "123"] },
514
- },
515
- required: ["name", "wtf"],
516
- additionalProperties: false,
517
- });
518
- });
519
-
520
- test("array (user example: name,addresses:array{string})", () => {
521
- expect(buildSchemaFromFields("name,addresses:array{string}")).toEqual({
522
- type: "object",
523
- properties: {
524
- name: { type: "string" },
525
- addresses: { type: "array", items: { type: "string" } },
526
- },
527
- required: ["name", "addresses"],
528
- additionalProperties: false,
529
- });
530
- });
531
-
532
- test("array shorthand (user example: name,authors:array)", () => {
533
- expect(buildSchemaFromFields("name,authors:array")).toEqual({
534
- type: "object",
535
- properties: {
536
- name: { type: "string" },
537
- authors: { type: "array", items: { type: "string" } },
538
- },
539
- required: ["name", "authors"],
540
- additionalProperties: false,
541
- });
542
- });
543
-
544
- test("all four types in one string", () => {
545
- expect(buildSchemaFromFields(
546
- "title, price:number, status:enum{a|b}, tags:array{string}",
547
- )).toEqual({
548
- type: "object",
549
- properties: {
550
- title: { type: "string" },
551
- price: { type: "number" },
552
- status: { type: "string", enum: ["a", "b"] },
553
- tags: { type: "array", items: { type: "string" } },
554
- },
555
- required: ["title", "price", "status", "tags"],
556
- additionalProperties: false,
557
- });
558
- });
559
-
560
- test("realistic product schema", () => {
561
- expect(buildSchemaFromFields(
562
- "id, name, price:number, in_stock:boolean, tags:array{string}, condition:enum{new|used|refurbished}",
563
- )).toEqual({
564
- type: "object",
565
- properties: {
566
- id: { type: "string" },
567
- name: { type: "string" },
568
- price: { type: "number" },
569
- in_stock: { type: "boolean" },
570
- tags: { type: "array", items: { type: "string" } },
571
- condition: { type: "string", enum: ["new", "used", "refurbished"] },
572
- },
573
- required: ["id", "name", "price", "in_stock", "tags", "condition"],
574
- additionalProperties: false,
575
- });
576
- });
577
-
578
- test("realistic article schema", () => {
579
- expect(buildSchemaFromFields(
580
- "title, author, word_count:integer, published:boolean, status:enum{draft|review|published}",
581
- )).toEqual({
582
- type: "object",
583
- properties: {
584
- title: { type: "string" },
585
- author: { type: "string" },
586
- word_count: { type: "integer" },
587
- published: { type: "boolean" },
588
- status: { type: "string", enum: ["draft", "review", "published"] },
589
- },
590
- required: ["title", "author", "word_count", "published", "status"],
591
- additionalProperties: false,
592
- });
593
- });
594
-
595
- test("numeric-looking enum values stay as strings in schema", () => {
596
- const schema = buildSchemaFromFields("rating:enum{1|2|3|4|5}") as {
597
- properties: { rating: { enum: unknown[] } };
598
- };
599
- expect(schema.properties.rating.enum).toEqual(["1", "2", "3", "4", "5"]);
600
- });
601
- });
602
-
603
- // ---------------------------------------------------------------------------
604
- // extract() mutual-exclusion guard
605
- // ---------------------------------------------------------------------------
606
-
607
- describe("extract() schema mutual exclusion", () => {
608
- const mockStrategy = () => ({
609
- name: "mock",
610
- run: async () => ({
611
- data: {},
612
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
613
- }),
614
- });
615
-
616
- test("error message tells you they are mutually exclusive", async () => {
617
- const { extract } = await import("./extract");
618
- const result = await extract({
619
- artifacts: [],
620
- schema: { type: "object", properties: {}, required: [] },
621
- fields: "title",
622
- strategy: mockStrategy(),
623
- });
624
- expect(result.error).toBeDefined();
625
- expect(result.error?.message).toMatch(/mutually exclusive/);
626
- });
627
-
628
- test("error message when neither schema nor fields are provided", async () => {
629
- const { extract } = await import("./extract");
630
- const result = await extract({
631
- artifacts: [],
632
- strategy: mockStrategy(),
633
- } as Parameters<typeof extract>[0]);
634
- expect(result.error).toBeDefined();
635
- expect(result.error?.message).toMatch(/schema definition is required/);
636
- // Tells you what to use instead
637
- expect(result.error?.message).toMatch(/`schema`/);
638
- expect(result.error?.message).toMatch(/`fields`/);
639
- });
640
-
641
- test("succeeds with only schema", async () => {
642
- const { extract } = await import("./extract");
643
- const result = await extract({
644
- artifacts: [],
645
- schema: { type: "object", properties: {}, required: [] },
646
- strategy: mockStrategy(),
647
- });
648
- expect(result.error).toBeUndefined();
649
- });
650
-
651
- test("succeeds with only fields string", async () => {
652
- const { extract } = await import("./extract");
653
- const result = await extract({
654
- artifacts: [],
655
- fields: "title",
656
- strategy: mockStrategy(),
657
- });
658
- expect(result.error).toBeUndefined();
659
- });
660
-
661
- test("succeeds with fields including enum and array", async () => {
662
- const { extract } = await import("./extract");
663
- const result = await extract({
664
- artifacts: [],
665
- fields: "title, status:enum{a|b}, tags:array{string}",
666
- strategy: mockStrategy(),
667
- });
668
- expect(result.error).toBeUndefined();
669
- });
670
-
671
- test("invalid fields string surfaces as error on result", async () => {
672
- const { extract } = await import("./extract");
673
- const result = await extract({
674
- artifacts: [],
675
- fields: "title:badtype",
676
- strategy: mockStrategy(),
677
- });
678
- expect(result.error).toBeDefined();
679
- expect(result.error?.message).toMatch(/Unknown type "badtype"/);
680
- });
681
- });