@struktur/sdk 0.1.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 (96) hide show
  1. package/README.md +79 -0
  2. package/package.json +33 -0
  3. package/src/artifacts/AGENTS.md +16 -0
  4. package/src/artifacts/fileToArtifact.test.ts +37 -0
  5. package/src/artifacts/fileToArtifact.ts +44 -0
  6. package/src/artifacts/input.test.ts +243 -0
  7. package/src/artifacts/input.ts +360 -0
  8. package/src/artifacts/providers.test.ts +19 -0
  9. package/src/artifacts/providers.ts +7 -0
  10. package/src/artifacts/urlToArtifact.test.ts +23 -0
  11. package/src/artifacts/urlToArtifact.ts +19 -0
  12. package/src/auth/AGENTS.md +11 -0
  13. package/src/auth/config.test.ts +132 -0
  14. package/src/auth/config.ts +129 -0
  15. package/src/auth/tokens.test.ts +58 -0
  16. package/src/auth/tokens.ts +229 -0
  17. package/src/chunking/AGENTS.md +11 -0
  18. package/src/chunking/ArtifactBatcher.test.ts +22 -0
  19. package/src/chunking/ArtifactBatcher.ts +110 -0
  20. package/src/chunking/ArtifactSplitter.test.ts +38 -0
  21. package/src/chunking/ArtifactSplitter.ts +151 -0
  22. package/src/debug/AGENTS.md +79 -0
  23. package/src/debug/logger.test.ts +244 -0
  24. package/src/debug/logger.ts +211 -0
  25. package/src/extract.test.ts +22 -0
  26. package/src/extract.ts +114 -0
  27. package/src/fields.test.ts +663 -0
  28. package/src/fields.ts +239 -0
  29. package/src/index.test.ts +20 -0
  30. package/src/index.ts +93 -0
  31. package/src/llm/AGENTS.md +9 -0
  32. package/src/llm/LLMClient.test.ts +196 -0
  33. package/src/llm/LLMClient.ts +106 -0
  34. package/src/llm/RetryingRunner.test.ts +174 -0
  35. package/src/llm/RetryingRunner.ts +188 -0
  36. package/src/llm/message.test.ts +42 -0
  37. package/src/llm/message.ts +47 -0
  38. package/src/llm/models.test.ts +82 -0
  39. package/src/llm/models.ts +190 -0
  40. package/src/merge/AGENTS.md +6 -0
  41. package/src/merge/Deduplicator.test.ts +108 -0
  42. package/src/merge/Deduplicator.ts +45 -0
  43. package/src/merge/SmartDataMerger.test.ts +177 -0
  44. package/src/merge/SmartDataMerger.ts +56 -0
  45. package/src/parsers/AGENTS.md +58 -0
  46. package/src/parsers/collect.test.ts +56 -0
  47. package/src/parsers/collect.ts +31 -0
  48. package/src/parsers/index.ts +6 -0
  49. package/src/parsers/mime.test.ts +91 -0
  50. package/src/parsers/mime.ts +137 -0
  51. package/src/parsers/npm.ts +26 -0
  52. package/src/parsers/pdf.test.ts +394 -0
  53. package/src/parsers/pdf.ts +194 -0
  54. package/src/parsers/runner.test.ts +95 -0
  55. package/src/parsers/runner.ts +177 -0
  56. package/src/parsers/types.ts +29 -0
  57. package/src/prompts/AGENTS.md +8 -0
  58. package/src/prompts/DeduplicationPrompt.test.ts +41 -0
  59. package/src/prompts/DeduplicationPrompt.ts +37 -0
  60. package/src/prompts/ExtractorPrompt.test.ts +21 -0
  61. package/src/prompts/ExtractorPrompt.ts +72 -0
  62. package/src/prompts/ParallelMergerPrompt.test.ts +8 -0
  63. package/src/prompts/ParallelMergerPrompt.ts +37 -0
  64. package/src/prompts/SequentialExtractorPrompt.test.ts +24 -0
  65. package/src/prompts/SequentialExtractorPrompt.ts +82 -0
  66. package/src/prompts/formatArtifacts.test.ts +39 -0
  67. package/src/prompts/formatArtifacts.ts +46 -0
  68. package/src/strategies/AGENTS.md +6 -0
  69. package/src/strategies/DoublePassAutoMergeStrategy.test.ts +53 -0
  70. package/src/strategies/DoublePassAutoMergeStrategy.ts +270 -0
  71. package/src/strategies/DoublePassStrategy.test.ts +48 -0
  72. package/src/strategies/DoublePassStrategy.ts +179 -0
  73. package/src/strategies/ParallelAutoMergeStrategy.test.ts +152 -0
  74. package/src/strategies/ParallelAutoMergeStrategy.ts +241 -0
  75. package/src/strategies/ParallelStrategy.test.ts +61 -0
  76. package/src/strategies/ParallelStrategy.ts +157 -0
  77. package/src/strategies/SequentialAutoMergeStrategy.test.ts +66 -0
  78. package/src/strategies/SequentialAutoMergeStrategy.ts +222 -0
  79. package/src/strategies/SequentialStrategy.test.ts +53 -0
  80. package/src/strategies/SequentialStrategy.ts +119 -0
  81. package/src/strategies/SimpleStrategy.test.ts +46 -0
  82. package/src/strategies/SimpleStrategy.ts +74 -0
  83. package/src/strategies/concurrency.test.ts +16 -0
  84. package/src/strategies/concurrency.ts +14 -0
  85. package/src/strategies/index.test.ts +20 -0
  86. package/src/strategies/index.ts +7 -0
  87. package/src/strategies/utils.test.ts +76 -0
  88. package/src/strategies/utils.ts +56 -0
  89. package/src/tokenization.test.ts +119 -0
  90. package/src/tokenization.ts +71 -0
  91. package/src/types.test.ts +25 -0
  92. package/src/types.ts +116 -0
  93. package/src/validation/AGENTS.md +6 -0
  94. package/src/validation/validator.test.ts +172 -0
  95. package/src/validation/validator.ts +82 -0
  96. package/tsconfig.json +22 -0
@@ -0,0 +1,244 @@
1
+ import { test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { createDebugLogger } from "./logger";
3
+
4
+ let stderrOutput: string[];
5
+ const originalStderrWrite = process.stderr.write;
6
+
7
+ beforeEach(() => {
8
+ stderrOutput = [];
9
+ process.stderr.write = (chunk: unknown) => {
10
+ if (typeof chunk === "string") {
11
+ stderrOutput.push(chunk);
12
+ }
13
+ return true;
14
+ };
15
+ });
16
+
17
+ afterEach(() => {
18
+ process.stderr.write = originalStderrWrite;
19
+ });
20
+
21
+ test("createDebugLogger with enabled=false is a no-op", () => {
22
+ const logger = createDebugLogger(false);
23
+ logger.cliInit({ args: { test: true } });
24
+ expect(stderrOutput.length).toBe(0);
25
+ });
26
+
27
+ test("createDebugLogger with enabled=true logs to stderr", () => {
28
+ const logger = createDebugLogger(true);
29
+ logger.cliInit({ args: { test: true } });
30
+ expect(stderrOutput.length).toBe(1);
31
+ const parsed = JSON.parse(stderrOutput[0]!);
32
+ expect(parsed.type).toBe("cli_init");
33
+ expect(parsed.args).toEqual({ test: true });
34
+ expect(parsed.timestamp).toBeDefined();
35
+ });
36
+
37
+ test("cliInit logs correct type", () => {
38
+ const logger = createDebugLogger(true);
39
+ logger.cliInit({ args: { strategy: "simple" } });
40
+ const parsed = JSON.parse(stderrOutput[0]!);
41
+ expect(parsed.type).toBe("cli_init");
42
+ });
43
+
44
+ test("schemaLoaded logs source and size", () => {
45
+ const logger = createDebugLogger(true);
46
+ logger.schemaLoaded({ source: "file.json", schemaSize: 100 });
47
+ const parsed = JSON.parse(stderrOutput[0]!);
48
+ expect(parsed.type).toBe("schema_loaded");
49
+ expect(parsed.source).toBe("file.json");
50
+ expect(parsed.schemaSize).toBe(100);
51
+ });
52
+
53
+ test("artifactsLoaded logs artifact details", () => {
54
+ const logger = createDebugLogger(true);
55
+ logger.artifactsLoaded({
56
+ count: 2,
57
+ artifacts: [
58
+ { id: "a1", type: "text", contentCount: 1, tokens: 10 },
59
+ { id: "a2", type: "pdf", contentCount: 3 },
60
+ ],
61
+ totalTokens: 1010,
62
+ totalImages: 2,
63
+ });
64
+ const parsed = JSON.parse(stderrOutput[0]!);
65
+ expect(parsed.type).toBe("artifacts_loaded");
66
+ expect(parsed.count).toBe(2);
67
+ expect(parsed.totalTokens).toBe(1010);
68
+ expect(parsed.totalImages).toBe(2);
69
+ });
70
+
71
+ test("chunkingStart logs chunking parameters", () => {
72
+ const logger = createDebugLogger(true);
73
+ logger.chunkingStart({
74
+ artifactId: "a1",
75
+ totalTokens: 100,
76
+ maxTokens: 50,
77
+ maxImages: 5,
78
+ });
79
+ const parsed = JSON.parse(stderrOutput[0]!);
80
+ expect(parsed.type).toBe("chunking_start");
81
+ expect(parsed.artifactId).toBe("a1");
82
+ expect(parsed.maxTokens).toBe(50);
83
+ });
84
+
85
+ test("llmCallStart logs call details", () => {
86
+ const logger = createDebugLogger(true);
87
+ logger.llmCallStart({
88
+ callId: "call-1",
89
+ model: "gpt-4",
90
+ schemaName: "extract",
91
+ systemLength: 100,
92
+ userLength: 200,
93
+ artifactCount: 3,
94
+ });
95
+ const parsed = JSON.parse(stderrOutput[0]!);
96
+ expect(parsed.type).toBe("llm_call_start");
97
+ expect(parsed.callId).toBe("call-1");
98
+ expect(parsed.artifactCount).toBe(3);
99
+ });
100
+
101
+ test("llmCallComplete logs success with duration", () => {
102
+ const logger = createDebugLogger(true);
103
+ logger.llmCallComplete({
104
+ callId: "call-1",
105
+ success: true,
106
+ inputTokens: 100,
107
+ outputTokens: 50,
108
+ totalTokens: 150,
109
+ durationMs: 1234,
110
+ });
111
+ const parsed = JSON.parse(stderrOutput[0]!);
112
+ expect(parsed.type).toBe("llm_call_complete");
113
+ expect(parsed.success).toBe(true);
114
+ expect(parsed.durationMs).toBe(1234);
115
+ });
116
+
117
+ test("llmCallComplete logs failure with error", () => {
118
+ const logger = createDebugLogger(true);
119
+ logger.llmCallComplete({
120
+ callId: "call-1",
121
+ success: false,
122
+ inputTokens: 100,
123
+ outputTokens: 0,
124
+ totalTokens: 100,
125
+ error: "API error",
126
+ });
127
+ const parsed = JSON.parse(stderrOutput[0]!);
128
+ expect(parsed.success).toBe(false);
129
+ expect(parsed.error).toBe("API error");
130
+ });
131
+
132
+ test("retry logs retry attempt", () => {
133
+ const logger = createDebugLogger(true);
134
+ logger.retry({
135
+ callId: "call-1",
136
+ attempt: 2,
137
+ maxAttempts: 3,
138
+ reason: "schema_validation_failed",
139
+ });
140
+ const parsed = JSON.parse(stderrOutput[0]!);
141
+ expect(parsed.type).toBe("retry");
142
+ expect(parsed.attempt).toBe(2);
143
+ expect(parsed.reason).toBe("schema_validation_failed");
144
+ });
145
+
146
+ test("validationStart logs validation attempt", () => {
147
+ const logger = createDebugLogger(true);
148
+ logger.validationStart({
149
+ callId: "call-1",
150
+ attempt: 1,
151
+ maxAttempts: 3,
152
+ strict: false,
153
+ });
154
+ const parsed = JSON.parse(stderrOutput[0]!);
155
+ expect(parsed.type).toBe("validation_start");
156
+ expect(parsed.strict).toBe(false);
157
+ });
158
+
159
+ test("validationSuccess logs successful validation", () => {
160
+ const logger = createDebugLogger(true);
161
+ logger.validationSuccess({ callId: "call-1", attempt: 1 });
162
+ const parsed = JSON.parse(stderrOutput[0]!);
163
+ expect(parsed.type).toBe("validation_success");
164
+ });
165
+
166
+ test("validationFailed logs validation errors", () => {
167
+ const logger = createDebugLogger(true);
168
+ logger.validationFailed({
169
+ callId: "call-1",
170
+ attempt: 1,
171
+ errors: [{ keyword: "required", message: "missing field" }],
172
+ });
173
+ const parsed = JSON.parse(stderrOutput[0]!);
174
+ expect(parsed.type).toBe("validation_failed");
175
+ expect(parsed.errors).toBeDefined();
176
+ });
177
+
178
+ test("mergeStart logs merge operation", () => {
179
+ const logger = createDebugLogger(true);
180
+ logger.mergeStart({
181
+ mergeId: "merge-1",
182
+ inputCount: 3,
183
+ strategy: "parallel",
184
+ });
185
+ const parsed = JSON.parse(stderrOutput[0]!);
186
+ expect(parsed.type).toBe("merge_start");
187
+ expect(parsed.inputCount).toBe(3);
188
+ });
189
+
190
+ test("mergeComplete logs merge result", () => {
191
+ const logger = createDebugLogger(true);
192
+ logger.mergeComplete({ mergeId: "merge-1", success: true });
193
+ const parsed = JSON.parse(stderrOutput[0]!);
194
+ expect(parsed.type).toBe("merge_complete");
195
+ expect(parsed.success).toBe(true);
196
+ });
197
+
198
+ test("dedupeStart logs deduplication start", () => {
199
+ const logger = createDebugLogger(true);
200
+ logger.dedupeStart({ dedupeId: "dedupe-1", itemCount: 10 });
201
+ const parsed = JSON.parse(stderrOutput[0]!);
202
+ expect(parsed.type).toBe("dedupe_start");
203
+ expect(parsed.itemCount).toBe(10);
204
+ });
205
+
206
+ test("dedupeComplete logs deduplication result", () => {
207
+ const logger = createDebugLogger(true);
208
+ logger.dedupeComplete({
209
+ dedupeId: "dedupe-1",
210
+ duplicatesFound: 3,
211
+ itemsRemoved: 3,
212
+ });
213
+ const parsed = JSON.parse(stderrOutput[0]!);
214
+ expect(parsed.type).toBe("dedupe_complete");
215
+ expect(parsed.duplicatesFound).toBe(3);
216
+ });
217
+
218
+ test("extractionComplete logs final result", () => {
219
+ const logger = createDebugLogger(true);
220
+ logger.extractionComplete({
221
+ success: true,
222
+ totalInputTokens: 100,
223
+ totalOutputTokens: 50,
224
+ totalTokens: 150,
225
+ });
226
+ const parsed = JSON.parse(stderrOutput[0]!);
227
+ expect(parsed.type).toBe("extraction_complete");
228
+ expect(parsed.success).toBe(true);
229
+ });
230
+
231
+ test("smartMergeField logs field merge operation", () => {
232
+ const logger = createDebugLogger(true);
233
+ logger.smartMergeField({
234
+ mergeId: "merge-1",
235
+ field: "items",
236
+ operation: "merge_arrays",
237
+ leftCount: 5,
238
+ rightCount: 3,
239
+ resultCount: 8,
240
+ });
241
+ const parsed = JSON.parse(stderrOutput[0]!);
242
+ expect(parsed.type).toBe("smart_merge_field");
243
+ expect(parsed.operation).toBe("merge_arrays");
244
+ });
@@ -0,0 +1,211 @@
1
+ import type { Artifact, ArtifactContent, ExtractionEvents, Usage, StepInfo, ProgressInfo, RetryInfo, TokenUsageInfo } from "../types";
2
+
3
+ export type DebugLogger = ReturnType<typeof createDebugLogger>;
4
+
5
+ export const createDebugLogger = (enabled: boolean) => {
6
+ const log = (entry: Record<string, unknown>) => {
7
+ if (!enabled) return;
8
+ const timestamp = new Date().toISOString();
9
+ const logEntry = { timestamp, ...entry };
10
+ process.stderr.write(JSON.stringify(logEntry) + "\n");
11
+ };
12
+
13
+ return {
14
+ // CLI initialization
15
+ cliInit: (data: { args: Record<string, unknown> }) => {
16
+ log({ type: "cli_init", ...data });
17
+ },
18
+
19
+ schemaLoaded: (data: { source: string; schemaSize: number }) => {
20
+ log({ type: "schema_loaded", ...data });
21
+ },
22
+
23
+ artifactsLoaded: (data: {
24
+ count: number;
25
+ artifacts: Array<{ id: string; type: string; contentCount: number; tokens?: number }>;
26
+ totalTokens: number;
27
+ totalImages: number;
28
+ }) => {
29
+ log({ type: "artifacts_loaded", ...data });
30
+ },
31
+
32
+ modelResolved: (data: { modelSpec: string; resolvedModel: string }) => {
33
+ log({ type: "model_resolved", ...data });
34
+ },
35
+
36
+ strategyCreated: (data: { strategy: string; config: Record<string, unknown> }) => {
37
+ log({ type: "strategy_created", ...data });
38
+ },
39
+
40
+ // Chunking
41
+ chunkingStart: (data: {
42
+ artifactId: string;
43
+ totalTokens: number;
44
+ maxTokens: number;
45
+ maxImages?: number;
46
+ }) => {
47
+ log({ type: "chunking_start", ...data });
48
+ },
49
+
50
+ chunkingSplit: (data: {
51
+ artifactId: string;
52
+ originalContentCount: number;
53
+ splitContentCount: number;
54
+ splitReason: "text_too_long" | "content_limit";
55
+ originalTokens: number;
56
+ chunkSize: number;
57
+ }) => {
58
+ log({ type: "chunking_split", ...data });
59
+ },
60
+
61
+ chunkingResult: (data: {
62
+ artifactId: string;
63
+ chunksCreated: number;
64
+ chunkSizes: number[];
65
+ }) => {
66
+ log({ type: "chunking_result", ...data });
67
+ },
68
+
69
+ batchingStart: (data: {
70
+ totalArtifacts: number;
71
+ maxTokens: number;
72
+ maxImages?: number;
73
+ modelMaxTokens?: number;
74
+ effectiveMaxTokens: number;
75
+ }) => {
76
+ log({ type: "batching_start", ...data });
77
+ },
78
+
79
+ batchCreated: (data: {
80
+ batchIndex: number;
81
+ artifactCount: number;
82
+ totalTokens: number;
83
+ totalImages: number;
84
+ artifactIds: string[];
85
+ }) => {
86
+ log({ type: "batch_created", ...data });
87
+ },
88
+
89
+ batchingComplete: (data: {
90
+ totalBatches: number;
91
+ batches: Array<{ index: number; artifactCount: number; tokens: number; images: number }>;
92
+ }) => {
93
+ log({ type: "batching_complete", ...data });
94
+ },
95
+
96
+ // Strategy execution
97
+ strategyRunStart: (data: { strategy: string; estimatedSteps: number; artifactCount: number }) => {
98
+ log({ type: "strategy_run_start", ...data });
99
+ },
100
+
101
+ step: (data: StepInfo & { strategy: string }) => {
102
+ log({ type: "step", ...data });
103
+ },
104
+
105
+ progress: (data: ProgressInfo & { strategy: string; context?: string }) => {
106
+ log({ type: "progress", ...data });
107
+ },
108
+
109
+ // LLM calls
110
+ llmCallStart: (data: {
111
+ callId: string;
112
+ model: string;
113
+ schemaName?: string;
114
+ systemLength: number;
115
+ userLength: number;
116
+ artifactCount: number;
117
+ }) => {
118
+ log({ type: "llm_call_start", ...data });
119
+ },
120
+
121
+ llmCallComplete: (data: {
122
+ callId: string;
123
+ success: boolean;
124
+ inputTokens: number;
125
+ outputTokens: number;
126
+ totalTokens: number;
127
+ durationMs?: number;
128
+ error?: string;
129
+ }) => {
130
+ log({ type: "llm_call_complete", ...data });
131
+ },
132
+
133
+ // Retry events
134
+ retry: (data: RetryInfo & { callId: string }) => {
135
+ log({ type: "retry", ...data });
136
+ },
137
+
138
+ // Validation
139
+ validationStart: (data: { callId: string; attempt: number; maxAttempts: number; strict: boolean }) => {
140
+ log({ type: "validation_start", ...data });
141
+ },
142
+
143
+ validationSuccess: (data: { callId: string; attempt: number }) => {
144
+ log({ type: "validation_success", ...data });
145
+ },
146
+
147
+ validationFailed: (data: { callId: string; attempt: number; errors: unknown[] }) => {
148
+ log({ type: "validation_failed", ...data });
149
+ },
150
+
151
+ // Merging
152
+ mergeStart: (data: { mergeId: string; inputCount: number; strategy: string }) => {
153
+ log({ type: "merge_start", ...data });
154
+ },
155
+
156
+ mergeComplete: (data: { mergeId: string; success: boolean; error?: string }) => {
157
+ log({ type: "merge_complete", ...data });
158
+ },
159
+
160
+ // Deduplication
161
+ dedupeStart: (data: { dedupeId: string; itemCount: number }) => {
162
+ log({ type: "dedupe_start", ...data });
163
+ },
164
+
165
+ dedupeComplete: (data: { dedupeId: string; duplicatesFound: number; itemsRemoved: number }) => {
166
+ log({ type: "dedupe_complete", ...data });
167
+ },
168
+
169
+ // Token usage tracking
170
+ tokenUsage: (data: TokenUsageInfo & { context: string }) => {
171
+ log({ type: "token_usage", ...data });
172
+ },
173
+
174
+ // Results
175
+ extractionComplete: (data: {
176
+ success: boolean;
177
+ totalInputTokens: number;
178
+ totalOutputTokens: number;
179
+ totalTokens: number;
180
+ error?: string;
181
+ }) => {
182
+ log({ type: "extraction_complete", ...data });
183
+ },
184
+
185
+ // Prompt details (verbose)
186
+ promptSystem: (data: { callId: string; system: string }) => {
187
+ log({ type: "prompt_system", ...data });
188
+ },
189
+
190
+ promptUser: (data: { callId: string; user: unknown }) => {
191
+ log({ type: "prompt_user", ...data });
192
+ },
193
+
194
+ // Raw response
195
+ rawResponse: (data: { callId: string; response: unknown }) => {
196
+ log({ type: "raw_response", ...data });
197
+ },
198
+
199
+ // Smart merge details
200
+ smartMergeField: (data: {
201
+ mergeId: string;
202
+ field: string;
203
+ operation: "merge_arrays" | "merge_objects" | "replace" | "concat";
204
+ leftCount?: number;
205
+ rightCount?: number;
206
+ resultCount?: number;
207
+ }) => {
208
+ log({ type: "smart_merge_field", ...data });
209
+ },
210
+ };
211
+ };
@@ -0,0 +1,22 @@
1
+ import { test, expect } from "bun:test";
2
+ import { extract } from "./extract";
3
+ import type { ExtractionStrategy, ExtractionOptions } from "./types";
4
+
5
+ test("extract delegates to strategy", async () => {
6
+ const strategy: ExtractionStrategy<{ ok: boolean }> = {
7
+ name: "mock",
8
+ run: async () => ({
9
+ data: { ok: true },
10
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
11
+ }),
12
+ };
13
+
14
+ const options: ExtractionOptions<{ ok: boolean }> = {
15
+ artifacts: [],
16
+ schema: {},
17
+ strategy,
18
+ };
19
+
20
+ const result = await extract(options);
21
+ expect(result.data.ok).toBe(true);
22
+ });
package/src/extract.ts ADDED
@@ -0,0 +1,114 @@
1
+ import type { ExtractionOptions, ExtractionResult } from "./types";
2
+ import { buildSchemaFromFields } from "./fields";
3
+
4
+ const emptyUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
5
+
6
+ /**
7
+ * Resolve and validate the schema from ExtractionOptions.
8
+ * Exactly one of `schema` or `fields` must be provided.
9
+ */
10
+ const resolveSchema = <T>(options: ExtractionOptions<T>) => {
11
+ const hasSchema = options.schema !== undefined;
12
+ const hasFields = options.fields !== undefined;
13
+
14
+ if (hasSchema && hasFields) {
15
+ throw new Error(
16
+ "Provide either `schema` or `fields`, not both. They are mutually exclusive.",
17
+ );
18
+ }
19
+
20
+ if (!hasSchema && !hasFields) {
21
+ throw new Error(
22
+ "A schema definition is required. Provide `schema` (a JSON Schema object) or `fields` (a shorthand fields string).",
23
+ );
24
+ }
25
+
26
+ if (hasFields) {
27
+ return buildSchemaFromFields(options.fields as string);
28
+ }
29
+
30
+ return options.schema as NonNullable<typeof options.schema>;
31
+ };
32
+
33
+ export const extract = async <T>(
34
+ options: ExtractionOptions<T>,
35
+ ): Promise<ExtractionResult<T>> => {
36
+ const debug = options.debug;
37
+
38
+ // Validate mutual exclusion and resolve the concrete schema early so that
39
+ // every strategy receives a fully-populated options object.
40
+ let resolvedOptions: ExtractionOptions<T>;
41
+ try {
42
+ const schema = resolveSchema(options);
43
+ resolvedOptions = { ...options, schema };
44
+ } catch (error) {
45
+ debug?.extractionComplete({
46
+ success: false,
47
+ totalInputTokens: 0,
48
+ totalOutputTokens: 0,
49
+ totalTokens: 0,
50
+ error: (error as Error).message,
51
+ });
52
+ return {
53
+ data: null as unknown as T,
54
+ usage: emptyUsage,
55
+ error: error as Error,
56
+ };
57
+ }
58
+
59
+ try {
60
+ const total = resolvedOptions.strategy.getEstimatedSteps?.(resolvedOptions.artifacts);
61
+
62
+ debug?.strategyRunStart({
63
+ strategy: resolvedOptions.strategy.name,
64
+ estimatedSteps: total ?? 1,
65
+ artifactCount: resolvedOptions.artifacts.length,
66
+ });
67
+
68
+ await resolvedOptions.events?.onStep?.({ step: 1, total, label: "start" });
69
+ debug?.step({
70
+ step: 1,
71
+ total,
72
+ label: "start",
73
+ strategy: resolvedOptions.strategy.name,
74
+ });
75
+
76
+ const result = await resolvedOptions.strategy.run(resolvedOptions);
77
+
78
+ await resolvedOptions.events?.onStep?.({
79
+ step: total ?? 1,
80
+ total,
81
+ label: "complete",
82
+ });
83
+ debug?.step({
84
+ step: total ?? 1,
85
+ total,
86
+ label: "complete",
87
+ strategy: resolvedOptions.strategy.name,
88
+ });
89
+
90
+ debug?.extractionComplete({
91
+ success: !result.error,
92
+ totalInputTokens: result.usage.inputTokens,
93
+ totalOutputTokens: result.usage.outputTokens,
94
+ totalTokens: result.usage.totalTokens,
95
+ error: result.error?.message,
96
+ });
97
+
98
+ return result;
99
+ } catch (error) {
100
+ debug?.extractionComplete({
101
+ success: false,
102
+ totalInputTokens: 0,
103
+ totalOutputTokens: 0,
104
+ totalTokens: 0,
105
+ error: (error as Error).message,
106
+ });
107
+
108
+ return {
109
+ data: null as unknown as T,
110
+ usage: emptyUsage,
111
+ error: error as Error,
112
+ };
113
+ }
114
+ };