@happyvertical/smrt-ingestion 0.55.5 → 0.55.6

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 (42) hide show
  1. package/AGENTS.md +2 -0
  2. package/PROPOSALS.md +173 -0
  3. package/README.md +3 -0
  4. package/agents/execution.md +132 -0
  5. package/dist/chunks/execution-C6HJvl9e.js +7667 -0
  6. package/dist/chunks/execution-C6HJvl9e.js.map +1 -0
  7. package/dist/chunks/execution-internal-DWteDHM2.js +7 -0
  8. package/dist/chunks/execution-internal-DWteDHM2.js.map +1 -0
  9. package/dist/chunks/extraction-bseWZuWN.js +293 -0
  10. package/dist/chunks/extraction-bseWZuWN.js.map +1 -0
  11. package/dist/dto.d.ts +1 -0
  12. package/dist/dto.d.ts.map +1 -1
  13. package/dist/execution-contracts.d.ts +3 -0
  14. package/dist/execution-contracts.d.ts.map +1 -1
  15. package/dist/execution-internal.d.ts +7 -0
  16. package/dist/execution-internal.d.ts.map +1 -0
  17. package/dist/execution.d.ts +9 -1
  18. package/dist/execution.d.ts.map +1 -1
  19. package/dist/execution.js +1 -7614
  20. package/dist/extraction.d.ts.map +1 -1
  21. package/dist/manifest.json +1 -1
  22. package/dist/models.js +1 -1
  23. package/dist/models.js.map +1 -1
  24. package/dist/proposal-contracts.d.ts +85 -0
  25. package/dist/proposal-contracts.d.ts.map +1 -0
  26. package/dist/proposal-dto.d.ts +120 -0
  27. package/dist/proposal-dto.d.ts.map +1 -0
  28. package/dist/proposal-sdk.d.ts +12 -0
  29. package/dist/proposal-sdk.d.ts.map +1 -0
  30. package/dist/proposal-validation.d.ts +188 -0
  31. package/dist/proposal-validation.d.ts.map +1 -0
  32. package/dist/proposals.d.ts +41 -0
  33. package/dist/proposals.d.ts.map +1 -0
  34. package/dist/proposals.js +865 -0
  35. package/dist/proposals.js.map +1 -0
  36. package/dist/server.d.ts +32 -0
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +119 -289
  39. package/dist/server.js.map +1 -1
  40. package/dist/smrt-knowledge.json +24 -8
  41. package/package.json +19 -13
  42. package/dist/execution.js.map +0 -1
package/dist/server.js CHANGED
@@ -1,300 +1,14 @@
1
1
  import "./models.js";
2
- import { a as providerIdentity, i as validateExtractionRequest, n as emptyExtraction, t as ExtractionFailure } from "./chunks/extraction-providers-C66ADDVe.js";
2
+ import { n as generationSnapshotRead } from "./chunks/execution-internal-DWteDHM2.js";
3
+ import { n as extractAnalysis, r as proposeDocumentSplits, t as createSDKExtractionAdapter } from "./chunks/extraction-bseWZuWN.js";
3
4
  import { detectEngine, isEmbeddedDatabase, isTransientDatabaseError, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
4
5
  import { withTenant } from "@happyvertical/smrt-tenancy";
5
6
  import { createHash, randomUUID } from "node:crypto";
6
7
  import { Asset } from "@happyvertical/smrt-assets";
7
8
  import { SmrtJobCollection } from "@happyvertical/smrt-jobs";
8
- import { fork } from "node:child_process";
9
- import { dirname, join } from "node:path";
10
- import { fileURLToPath } from "node:url";
9
+ import { join } from "node:path";
11
10
  import { constants } from "node:fs";
12
11
  import { lstat, mkdir, open, readFile, readdir, rename, writeFile } from "node:fs/promises";
13
- //#region src/extraction.ts
14
- function createSDKExtractionAdapter(configuration) {
15
- try {
16
- configuration = structuredClone(configuration);
17
- } catch {
18
- throw new Error("Serializable SDK configuration required");
19
- }
20
- const maximum = configuration.maxConcurrentProcesses ?? 1;
21
- if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 32) throw new Error("Invalid extraction concurrency");
22
- let running = 0;
23
- if (configuration.nativeMemoryIsolation !== "host-enforced") throw new Error("Native memory isolation is required");
24
- for (const provider of [
25
- configuration.pdf,
26
- configuration.ocr,
27
- configuration.speech,
28
- configuration.vision
29
- ]) if (provider) provider.identity = providerIdentity(provider.identity);
30
- if (configuration.pdf?.provider === "auto" || configuration.ocr?.identity.provider === "auto") throw new Error("Pin extraction providers");
31
- return { async extract(request) {
32
- let latest = emptyExtraction(request);
33
- try {
34
- validateExtractionRequest(request);
35
- } catch (caught) {
36
- if (!(caught instanceof ExtractionFailure)) throw caught;
37
- latest.errors.push({
38
- category: caught.category,
39
- location: { kind: "source" }
40
- });
41
- return latest;
42
- }
43
- if (request.evidence.mediaType.toLowerCase().startsWith("video/")) {
44
- latest.status = "unsupported";
45
- latest.errors.push({
46
- category: "unsupported_type",
47
- location: { kind: "source" }
48
- });
49
- return latest;
50
- }
51
- if (request.signal?.aborted) {
52
- latest.errors.push({
53
- category: "cancelled",
54
- location: { kind: "source" }
55
- });
56
- return latest;
57
- }
58
- if (running >= maximum) {
59
- latest.errors.push({
60
- category: "limit",
61
- location: { kind: "source" }
62
- });
63
- return latest;
64
- }
65
- running++;
66
- return new Promise((resolve) => {
67
- const child = fork(join(dirname(fileURLToPath(import.meta.url)), "extraction-worker.js"), [], {
68
- serialization: "advanced",
69
- stdio: [
70
- "ignore",
71
- "ignore",
72
- "ignore",
73
- "ipc"
74
- ],
75
- execArgv: [`--max-old-space-size=${request.limits.workerHeapMb}`],
76
- detached: process.platform !== "win32"
77
- });
78
- let done = false;
79
- const stop = () => {
80
- try {
81
- if (process.platform !== "win32" && child.pid) process.kill(-child.pid, "SIGKILL");
82
- else child.kill("SIGKILL");
83
- } catch {
84
- child.kill("SIGKILL");
85
- }
86
- };
87
- const finish = (category) => {
88
- if (done) return;
89
- done = true;
90
- running--;
91
- clearTimeout(timer);
92
- request.signal?.removeEventListener("abort", abort);
93
- stop();
94
- if (category) {
95
- latest.errors.push({
96
- category,
97
- location: { kind: "source" }
98
- });
99
- latest.status = latest.segments.length ? "partial" : "failed";
100
- latest.truncated = true;
101
- latest.omitted.push({ kind: "source" });
102
- }
103
- resolve(latest);
104
- };
105
- const abort = () => finish("cancelled");
106
- const timer = setTimeout(() => finish("timeout"), request.limits.timeoutMs);
107
- request.signal?.addEventListener("abort", abort, { once: true });
108
- child.on("error", () => finish("unavailable"));
109
- child.on("exit", () => {
110
- if (!done) finish("unavailable");
111
- });
112
- child.on("message", async (message) => {
113
- if (done) return;
114
- if (message.kind === "authorize" && Number.isSafeInteger(message.id)) {
115
- try {
116
- await request.beforeProviderCall?.();
117
- if (!done) child.send({
118
- kind: "authorized",
119
- id: message.id
120
- });
121
- } catch {
122
- finish("cancelled");
123
- }
124
- return;
125
- }
126
- if ((message.kind === "progress" || message.kind === "result") && message.result) {
127
- if (Buffer.byteLength(JSON.stringify(message.result)) > request.limits.maxOutputBytes + 65536) return finish("limit");
128
- latest = message.result;
129
- if (message.kind === "result") finish();
130
- } else finish("unavailable");
131
- });
132
- const { signal: _signal, beforeProviderCall: _authorize, ...wireRequest } = request;
133
- child.send({
134
- request: wireRequest,
135
- configuration
136
- }, (error) => {
137
- if (error) finish("unavailable");
138
- });
139
- if (request.signal?.aborted) abort();
140
- });
141
- } };
142
- }
143
- function validateAndNormalizeExtractionOutput(result, request) {
144
- const numeric = (value) => Number.isFinite(value) && value >= 0;
145
- const location = (value) => {
146
- if (!value || typeof value !== "object") return false;
147
- if (value.kind === "source") return true;
148
- if (value.kind === "page") return Number.isSafeInteger(value.page) && value.page > 0;
149
- if (value.kind === "pages") return Number.isSafeInteger(value.startPage) && value.startPage > 0 && Number.isSafeInteger(value.endPage) && value.endPage >= value.startPage;
150
- return value.kind === "time" && numeric(value.startMs) && numeric(value.endMs) && value.endMs >= value.startMs;
151
- };
152
- if (!result || ![
153
- "complete",
154
- "partial",
155
- "failed",
156
- "unsupported"
157
- ].includes(result.status) || result.automaticActionEligible !== false || result.configurationRevision !== request.configurationRevision || !result.evidence || Object.entries(request.evidence).some(([key, value]) => result.evidence[key] !== value) || typeof result.truncated !== "boolean" || !Array.isArray(result.capabilities) || result.capabilities.some((capability) => !capability || typeof capability.confidence !== "boolean" || typeof capability.boxes !== "boolean" || ![
158
- "source",
159
- "page",
160
- "time"
161
- ].includes(capability.location) || !["unknown", "reported"].includes(capability.truncation) || !["unknown", "reported"].includes(capability.usage)) || !result.limits || Object.entries(request.limits).some(([key, value]) => result.limits[key] !== value) || !Array.isArray(result.segments) || !Array.isArray(result.errors) || !Array.isArray(result.omitted) || result.status === "complete" && (result.truncated || result.errors.length > 0 || result.omitted.length > 0) || result.omitted.some((value) => !location(value)) || result.segments.some((segment) => typeof segment.text !== "string" || !location(segment.location) || segment.location.kind === "pages" || segment.provenance?.provider === "unlimited-ocr" && (segment.confidence !== null || segment.boxes !== null) || !segment.provenance || [
162
- segment.provenance.provider,
163
- segment.provenance.model,
164
- segment.provenance.version
165
- ].some((value) => typeof value !== "string" || !value) || segment.confidence !== null && (!segment.confidence || !numeric(segment.confidence.value) || segment.confidence.value > 1 || segment.confidence.scale !== "0-1" || segment.confidence.calibration !== "unknown") || segment.boxes !== null && (!Array.isArray(segment.boxes) || segment.boxes.some((box) => typeof box.text !== "string" || ![
166
- box.x,
167
- box.y,
168
- box.width,
169
- box.height
170
- ].every(numeric)))) || result.errors.some((error) => ![
171
- "unsupported_type",
172
- "malformed_output",
173
- "limit",
174
- "timeout",
175
- "unavailable",
176
- "authentication",
177
- "cancelled",
178
- "integrity"
179
- ].includes(error.category) || !location(error.location)) || !result.usage || Object.values(result.usage).some((value) => !numeric(value))) throw new Error("Invalid extraction output");
180
- for (const segment of result.segments) segment.provenance = providerIdentity(segment.provenance);
181
- result.capabilities = result.capabilities.map((capability) => ({
182
- ...providerIdentity(capability),
183
- confidence: capability.confidence,
184
- boxes: capability.boxes,
185
- location: capability.location,
186
- truncation: capability.truncation,
187
- usage: capability.usage
188
- }));
189
- }
190
- async function extractAnalysis(service, lease, adapter, options) {
191
- const snapshot = await service.getAnalysisInput(lease);
192
- const persisted = snapshot.configuration.extraction;
193
- if (!persisted || persisted.configurationRevision !== options.configurationRevision || !persisted.limits || Object.keys(options.limits).some((key) => persisted.limits?.[key] !== options.limits[key]) || Object.keys(persisted.limits).length !== Object.keys(options.limits).length) throw new Error("Extraction configuration mismatch");
194
- const evidence = snapshot.evidence;
195
- const boundOptions = {
196
- ...options,
197
- limits: structuredClone(options.limits)
198
- };
199
- const results = [];
200
- let limited = false;
201
- const publication = (reserve = false) => {
202
- const allComplete = !limited && results.length === evidence.length && results.every((result) => result.status === "complete");
203
- const firstError = limited || reserve ? "limit" : results.flatMap((result) => result.errors)[0]?.category;
204
- return {
205
- status: reserve ? "needs_attention" : allComplete ? "completed" : results.some((result) => result.segments.length) ? "partial" : "needs_attention",
206
- provider: "smrt-ingestion-extraction",
207
- model: "none",
208
- version: "1",
209
- output: {
210
- configurationRevision: options.configurationRevision,
211
- results,
212
- ...limited || reserve ? {
213
- truncated: true,
214
- omittedEvidenceCount: evidence.length - results.length
215
- } : {}
216
- },
217
- usage: {},
218
- ...firstError ? { error: firstError } : {}
219
- };
220
- };
221
- const fits = (output) => Buffer.byteLength(JSON.stringify(output)) <= snapshot.maxOutputBytes;
222
- const minimum = {
223
- status: "needs_attention",
224
- provider: "smrt-ingestion-extraction",
225
- model: "none",
226
- version: "1",
227
- output: {},
228
- usage: {},
229
- error: "limit"
230
- };
231
- if (!fits(minimum)) return service.failAnalysis(lease, "limit");
232
- if (!fits(publication(true))) return service.completeAnalysis(lease, minimum);
233
- for (const part of evidence) {
234
- await service.getAnalysisInput(lease);
235
- const bytes = await service.readEvidence(lease.itemId, part.id);
236
- await service.getAnalysisInput(lease);
237
- const request = {
238
- ...boundOptions,
239
- evidence: part,
240
- bytes
241
- };
242
- const result = await adapter.extract({
243
- ...request,
244
- evidence: { ...part },
245
- limits: { ...request.limits },
246
- bytes: bytes.slice(),
247
- beforeProviderCall: async () => {
248
- await service.getAnalysisInput(lease);
249
- }
250
- });
251
- validateAndNormalizeExtractionOutput(result, request);
252
- results.push(result);
253
- if (!fits(publication(true))) {
254
- limited = true;
255
- result.status = "partial";
256
- result.truncated = true;
257
- result.errors.push({
258
- category: "limit",
259
- location: { kind: "source" }
260
- });
261
- while (result.segments.length && !fits(publication(true))) {
262
- const removed = result.segments.pop();
263
- if (removed) result.omitted.push(removed.location);
264
- }
265
- if (!fits(publication(true))) results.pop();
266
- }
267
- await service.getAnalysisInput(lease);
268
- if (limited || options.signal?.aborted) break;
269
- }
270
- return service.completeAnalysis(lease, publication());
271
- }
272
- function proposeDocumentSplits(result, revision, groups) {
273
- if (!Number.isSafeInteger(revision) || revision < 1 || !groups.length) throw new Error("Invalid split revision");
274
- const available = new Set(result.segments.flatMap((segment) => segment.location.kind === "page" ? [segment.location.page] : []));
275
- const seen = /* @__PURE__ */ new Set();
276
- for (const group of groups) {
277
- if (!group.length) throw new Error("Empty split");
278
- for (const page of group) {
279
- if (!Number.isSafeInteger(page) || !available.has(page) || seen.has(page)) throw new Error("Invalid split page");
280
- seen.add(page);
281
- }
282
- if (group.some((page, index) => index > 0 && page <= group[index - 1])) throw new Error("Unordered split");
283
- }
284
- const proposal = {
285
- revision,
286
- evidenceId: result.evidence.id,
287
- inputHash: result.evidence.contentHash,
288
- configurationRevision: result.configurationRevision,
289
- groups: groups.map((pages) => ({ pages: [...pages] })),
290
- status: "proposed"
291
- };
292
- return {
293
- ...proposal,
294
- digest: createHash("sha256").update(JSON.stringify(proposal)).digest("hex")
295
- };
296
- }
297
- //#endregion
298
12
  //#region src/sources/common.ts
299
13
  var SourceInputError = class extends Error {
300
14
  constructor(category) {
@@ -943,6 +657,26 @@ var IngestionService = class {
943
657
  async applyPlan(planId) {
944
658
  return (await this.execution()).applyPlan(planId);
945
659
  }
660
+ async proposals() {
661
+ if (!this.options.proposals) throw new Error("Intake proposals are not configured");
662
+ const { IngestionProposalService } = await import("./proposals.js");
663
+ return new IngestionProposalService(this, this.options, this.options.proposals);
664
+ }
665
+ async listHandlers(itemId) {
666
+ return (await this.proposals()).listHandlers(itemId);
667
+ }
668
+ async findCandidates(input) {
669
+ return (await this.proposals()).findCandidates(input);
670
+ }
671
+ async prepareGeneration(itemId, attemptId) {
672
+ return (await this.proposals()).prepareGeneration(itemId, attemptId);
673
+ }
674
+ async generateProposals(lease) {
675
+ return (await this.proposals()).generate(lease);
676
+ }
677
+ async previewGeneratedProposals(input) {
678
+ return (await this.proposals()).preview(input);
679
+ }
946
680
  now() {
947
681
  return this.options.now?.() ?? /* @__PURE__ */ new Date();
948
682
  }
@@ -1439,6 +1173,102 @@ var IngestionService = class {
1439
1173
  };
1440
1174
  });
1441
1175
  }
1176
+ async completedAnalysisSnapshot(db, itemId, attemptId, current) {
1177
+ await this.lock(db, itemId);
1178
+ const item = await this.item(itemId, "process", db);
1179
+ if (item.cancelled) throw new Error("Analysis unavailable");
1180
+ if (attemptId === void 0) {
1181
+ if (!current) throw new Error("Historical attempt required");
1182
+ const [latest] = await this.rows(db, "intake_analyses", "item_id=? AND revision=?", [itemId, item.analysis_revision]);
1183
+ if (!latest?.current_attempt_id) throw new Error("Analysis unavailable");
1184
+ attemptId = String(latest.current_attempt_id);
1185
+ }
1186
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "item_id=? AND id=?", [itemId, attemptId]);
1187
+ if (!attempt || !["completed", "partial"].includes(String(attempt.state)) || !attempt.output_digest) throw new Error("Analysis unavailable");
1188
+ const [analysis] = await this.rows(db, "intake_analyses", "item_id=? AND id=?", [itemId, attempt.analysis_id]);
1189
+ if (!analysis || analysis.current_attempt_id !== attemptId || current && Number(item.analysis_revision) !== Number(analysis.revision)) throw new Error("Analysis unavailable");
1190
+ const data = object(analysis.data);
1191
+ const configuration = object(data.configuration);
1192
+ if (!Array.isArray(data.inputs) || !data.inputs.length || digest({
1193
+ inputs: data.inputs,
1194
+ configuration
1195
+ }) !== analysis.input_digest || digest(configuration) !== data.configDigest) throw new Error("Analysis input integrity");
1196
+ const resultData = object(attempt.data);
1197
+ const result = {
1198
+ status: resultData.status,
1199
+ provider: resultData.provider,
1200
+ model: resultData.model,
1201
+ version: resultData.version,
1202
+ output: resultData.output,
1203
+ usage: resultData.usage,
1204
+ ...Object.hasOwn(resultData, "error") ? { error: resultData.error } : {},
1205
+ ...Object.hasOwn(resultData, "confidence") ? { confidence: resultData.confidence } : {}
1206
+ };
1207
+ if (result.status !== attempt.state || digest(result) !== attempt.output_digest) throw new Error("Analysis output integrity");
1208
+ const evidence = [];
1209
+ for (const input of data.inputs) {
1210
+ const frozen = object(input);
1211
+ const [row] = await this.rows(db, "intake_evidence", "item_id=? AND id=? AND state='durable'", [itemId, frozen.id]);
1212
+ if (!row || row.content_hash !== frozen.hash) throw new Error("Analysis input integrity");
1213
+ evidence.push({
1214
+ id: String(row.id),
1215
+ partId: String(row.part_id),
1216
+ parentEvidenceId: row.parent_evidence_id ? String(row.parent_evidence_id) : null,
1217
+ mediaType: String(row.media_type),
1218
+ contentHash: String(row.content_hash),
1219
+ byteLength: Number(row.byte_length)
1220
+ });
1221
+ }
1222
+ if (new Set(evidence.map((entry) => entry.id)).size !== evidence.length) throw new Error("Analysis input integrity");
1223
+ await this.item(itemId, "process", db);
1224
+ return structuredClone({
1225
+ itemId,
1226
+ attemptId,
1227
+ revision: Number(analysis.revision),
1228
+ inputDigest: String(analysis.input_digest),
1229
+ outputDigest: String(attempt.output_digest),
1230
+ evidenceDigest: digest(data.inputs),
1231
+ configuration,
1232
+ evidence,
1233
+ result
1234
+ });
1235
+ }
1236
+ /** Detached successful current revision; always current-scope/process authorized. */
1237
+ async getCompletedAnalysis(itemId, attemptId) {
1238
+ const snapshot = await this.tx((db) => this.completedAnalysisSnapshot(db, itemId, attemptId, true));
1239
+ await this.point("analysis:snapshot-read");
1240
+ const read = async (db) => {
1241
+ const current = await this.completedAnalysisSnapshot(db, itemId, snapshot.attemptId, true);
1242
+ if (current.inputDigest !== snapshot.inputDigest || current.outputDigest !== snapshot.outputDigest) throw new Error("Analysis changed");
1243
+ return current;
1244
+ };
1245
+ if (snapshot.configuration.stage === "interpret") return (await this.proposals())[generationSnapshotRead](itemId, (work) => this.tx(work), read);
1246
+ return this.tx(read);
1247
+ }
1248
+ /** Historical source is readable only through its frozen pin on a live generation lease. */
1249
+ async getGenerationInput(lease) {
1250
+ const generation = await this.getAnalysisInput(lease);
1251
+ const configuration = generation.configuration;
1252
+ const pin = object(object(configuration.proposals).source);
1253
+ if (configuration.stage !== "interpret" || typeof pin.attemptId !== "string") throw new Error("Invalid generation source");
1254
+ const source = await this.tx((db) => this.completedAnalysisSnapshot(db, lease.itemId, String(pin.attemptId), false));
1255
+ for (const key of [
1256
+ "attemptId",
1257
+ "revision",
1258
+ "inputDigest",
1259
+ "outputDigest",
1260
+ "evidenceDigest"
1261
+ ]) if (pin[key] !== source[key]) throw new Error("Generation source changed");
1262
+ if (source.result.provider !== "smrt-ingestion-extraction" || source.configuration.stage === "interpret" || source.revision !== generation.revision - 1 || digest(generation.evidence.map((entry) => ({
1263
+ id: entry.id,
1264
+ hash: entry.contentHash
1265
+ }))) !== source.evidenceDigest) throw new Error("Generation evidence changed");
1266
+ await this.getAnalysisInput(lease);
1267
+ return {
1268
+ generation,
1269
+ source
1270
+ };
1271
+ }
1442
1272
  /** Reload immutable inputs only through the current live processing lease. */
1443
1273
  async getAnalysisInput(lease) {
1444
1274
  return this.tx(async (db) => {