@happyvertical/smrt-ingestion 0.55.6 → 0.55.8

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 (77) hide show
  1. package/AGENTS.md +13 -3
  2. package/FEEDBACK.md +156 -0
  3. package/README.md +15 -3
  4. package/REVIEW.md +137 -0
  5. package/dist/chunks/{execution-C6HJvl9e.js → execution-C7sTFXFU.js} +375 -11
  6. package/dist/chunks/execution-C7sTFXFU.js.map +1 -0
  7. package/dist/chunks/execution-internal-DWteDHM2.js.map +1 -1
  8. package/dist/chunks/feedback-C3OHD4sA.js +625 -0
  9. package/dist/chunks/feedback-C3OHD4sA.js.map +1 -0
  10. package/dist/chunks/{extraction-bseWZuWN.js → proposal-errors-z24VKWnQ.js} +10 -2
  11. package/dist/chunks/proposal-errors-z24VKWnQ.js.map +1 -0
  12. package/dist/dto.d.ts +2 -0
  13. package/dist/dto.d.ts.map +1 -1
  14. package/dist/execution-internal.d.ts +3 -1
  15. package/dist/execution-internal.d.ts.map +1 -1
  16. package/dist/execution.d.ts +15 -1
  17. package/dist/execution.d.ts.map +1 -1
  18. package/dist/execution.js +1 -1
  19. package/dist/feedback-contracts.d.ts +51 -0
  20. package/dist/feedback-contracts.d.ts.map +1 -0
  21. package/dist/feedback-dto.d.ts +106 -0
  22. package/dist/feedback-dto.d.ts.map +1 -0
  23. package/dist/feedback-internal.d.ts +19 -0
  24. package/dist/feedback-internal.d.ts.map +1 -0
  25. package/dist/feedback.d.ts +54 -0
  26. package/dist/feedback.d.ts.map +1 -0
  27. package/dist/manifest.json +34 -1
  28. package/dist/models.js +1 -1
  29. package/dist/models.js.map +1 -1
  30. package/dist/proposal-contracts.d.ts +4 -0
  31. package/dist/proposal-contracts.d.ts.map +1 -1
  32. package/dist/proposal-dto.d.ts +4 -0
  33. package/dist/proposal-dto.d.ts.map +1 -1
  34. package/dist/proposal-errors.d.ts +5 -0
  35. package/dist/proposal-errors.d.ts.map +1 -0
  36. package/dist/proposal-sdk.d.ts.map +1 -1
  37. package/dist/proposals.d.ts +4 -1
  38. package/dist/proposals.d.ts.map +1 -1
  39. package/dist/proposals.js +114 -15
  40. package/dist/proposals.js.map +1 -1
  41. package/dist/review-dto.d.ts +115 -0
  42. package/dist/review-dto.d.ts.map +1 -0
  43. package/dist/server.d.ts +24 -0
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +146 -54
  46. package/dist/server.js.map +1 -1
  47. package/dist/smrt-knowledge.json +97 -14
  48. package/dist/svelte/components/ActionReview.svelte +232 -0
  49. package/dist/svelte/components/ActionReview.svelte.d.ts +15 -0
  50. package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -0
  51. package/dist/svelte/components/EvidenceViewer.svelte +31 -0
  52. package/dist/svelte/components/EvidenceViewer.svelte.d.ts +8 -0
  53. package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -0
  54. package/dist/svelte/components/InboxView.svelte +119 -0
  55. package/dist/svelte/components/InboxView.svelte.d.ts +9 -0
  56. package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -0
  57. package/dist/svelte/components/IntakeInbox.svelte +11 -0
  58. package/dist/svelte/components/IntakeInbox.svelte.d.ts +10 -0
  59. package/dist/svelte/components/IntakeInbox.svelte.d.ts.map +1 -0
  60. package/dist/svelte/components/IntakeReview.svelte +11 -0
  61. package/dist/svelte/components/IntakeReview.svelte.d.ts +10 -0
  62. package/dist/svelte/components/IntakeReview.svelte.d.ts.map +1 -0
  63. package/dist/svelte/components/ReviewView.svelte +243 -0
  64. package/dist/svelte/components/ReviewView.svelte.d.ts +9 -0
  65. package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -0
  66. package/dist/svelte/i18n.d.ts +72 -0
  67. package/dist/svelte/i18n.d.ts.map +1 -0
  68. package/dist/svelte/i18n.js +72 -0
  69. package/dist/svelte/index.d.ts +5 -0
  70. package/dist/svelte/index.d.ts.map +1 -0
  71. package/dist/svelte/index.js +3 -0
  72. package/dist/svelte/view-utils.d.ts +13 -0
  73. package/dist/svelte/view-utils.d.ts.map +1 -0
  74. package/dist/svelte/view-utils.js +96 -0
  75. package/package.json +49 -22
  76. package/dist/chunks/execution-C6HJvl9e.js.map +0 -1
  77. package/dist/chunks/extraction-bseWZuWN.js.map +0 -1
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./models.js";
2
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
+ import { i as proposeDocumentSplits, n as createSDKExtractionAdapter, r as extractAnalysis, t as GenerationSnapshotStaleError } from "./chunks/proposal-errors-z24VKWnQ.js";
4
4
  import { detectEngine, isEmbeddedDatabase, isTransientDatabaseError, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
5
5
  import { withTenant } from "@happyvertical/smrt-tenancy";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -645,6 +645,9 @@ var IngestionService = class {
645
645
  async reconcileAction(actionId) {
646
646
  return (await this.execution()).reconcileAction(actionId);
647
647
  }
648
+ async listReviews(itemId, input = {}) {
649
+ return (await this.execution()).listReviews(itemId, input);
650
+ }
648
651
  async getAction(actionId) {
649
652
  return (await this.execution()).getAction(actionId);
650
653
  }
@@ -677,6 +680,28 @@ var IngestionService = class {
677
680
  async previewGeneratedProposals(input) {
678
681
  return (await this.proposals()).preview(input);
679
682
  }
683
+ async feedback() {
684
+ const { IntakeFeedbackService } = await import("./chunks/feedback-C3OHD4sA.js").then((n) => n.n);
685
+ return new IntakeFeedbackService(this.options);
686
+ }
687
+ async recordFeedback(input) {
688
+ return (await this.feedback()).record(input);
689
+ }
690
+ async observeFeedback(input) {
691
+ return (await this.feedback()).observe(input);
692
+ }
693
+ async retrieveFeedback(input) {
694
+ return (await this.proposals()).retrieveFeedback(input);
695
+ }
696
+ async suggestRoutingRule(input) {
697
+ return (await this.feedback()).suggest(input);
698
+ }
699
+ async getRoutingRule(input) {
700
+ return (await this.feedback()).readRule(input);
701
+ }
702
+ async adoptRoutingRule(input) {
703
+ return (await this.feedback()).adopt(input);
704
+ }
680
705
  now() {
681
706
  return this.options.now?.() ?? /* @__PURE__ */ new Date();
682
707
  }
@@ -1063,62 +1088,129 @@ var IngestionService = class {
1063
1088
  }
1064
1089
  /** Append an immutable input revision. Reprocessing never creates actions. */
1065
1090
  async analyze(itemId, configuration, requestKey) {
1091
+ const frozen = structuredClone(configuration);
1092
+ if (Object.hasOwn(frozen, "humanCorrection") && frozen.stage !== "interpret") throw new Error("Human corrections require authenticated split revision");
1093
+ return this.tx((db) => this.appendAnalysis(db, itemId, frozen, requestKey));
1094
+ }
1095
+ async appendAnalysis(db, itemId, configuration, requestKey) {
1066
1096
  nonempty(requestKey);
1067
1097
  const configDigest = digest(configuration);
1068
- return this.tx(async (db) => {
1069
- await this.lock(db, itemId);
1070
- const item = await this.item(itemId, "process", db);
1071
- if (item.receipt_state !== "ready" || item.cancelled) throw new Error("Intake not processable");
1072
- const replay = (await this.rows(db, "intake_analyses", "item_id=?", [itemId])).find((a) => object(a.data).requestKey === requestKey);
1073
- if (replay) {
1074
- if (object(replay.data).configDigest !== configDigest) throw new Error("Analysis request conflict");
1075
- return {
1076
- id: String(replay.id),
1077
- revision: Number(replay.revision)
1078
- };
1079
- }
1080
- const evidence = await this.rows(db, "intake_evidence", "item_id=? AND state='durable'", [itemId]);
1081
- await db.query("UPDATE intake_analysis_attempts SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1082
- await db.query("UPDATE intake_analyses SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1083
- const revision = Number(item.analysis_revision) + 1;
1084
- await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND revision<? AND state<>'completed'", this.scope.tenantId, this.scope.confidentialScopeId, itemId, revision);
1085
- const inputs = evidence.map((e) => ({
1086
- id: e.id,
1087
- hash: e.content_hash
1088
- })).sort((a, b) => String(a.id).localeCompare(String(b.id)));
1089
- const [analysis] = await this.insert(db, "intake_analyses", {
1090
- item_id: itemId,
1091
- revision,
1092
- fence: 0,
1093
- attempt_number: 0,
1094
- current_attempt_id: null,
1095
- state: "queued",
1096
- input_digest: digest({
1097
- inputs,
1098
- configuration
1099
- }),
1100
- data: canonical({
1101
- inputs,
1102
- configuration,
1103
- requestKey,
1104
- configDigest
1105
- })
1106
- });
1107
- await db.query("UPDATE intake_items SET analysis_revision=?,processing_state='queued' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", revision, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1108
- await this.insert(db, "intake_dispatches", {
1109
- item_id: itemId,
1110
- stage: "analyze",
1111
- revision,
1112
- state: "pending",
1113
- job_id: "",
1114
- deliveries: 0,
1115
- data: "{}"
1116
- }, "tenant_id,item_id,stage,revision");
1098
+ await this.lock(db, itemId);
1099
+ const item = await this.item(itemId, "process", db);
1100
+ if (item.receipt_state !== "ready" || item.cancelled) throw new Error("Intake not processable");
1101
+ const replay = (await this.rows(db, "intake_analyses", "item_id=?", [itemId])).find((a) => object(a.data).requestKey === requestKey);
1102
+ if (replay) {
1103
+ if (object(replay.data).configDigest !== configDigest) throw new Error("Analysis request conflict");
1117
1104
  return {
1118
- id: String(analysis.id),
1119
- revision
1105
+ id: String(replay.id),
1106
+ revision: Number(replay.revision)
1120
1107
  };
1108
+ }
1109
+ const evidence = await this.rows(db, "intake_evidence", "item_id=? AND state='durable'", [itemId]);
1110
+ await db.query("UPDATE intake_analysis_attempts SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1111
+ await db.query("UPDATE intake_analyses SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1112
+ const revision = Number(item.analysis_revision) + 1;
1113
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND revision<? AND state<>'completed'", this.scope.tenantId, this.scope.confidentialScopeId, itemId, revision);
1114
+ const inputs = evidence.map((e) => ({
1115
+ id: e.id,
1116
+ hash: e.content_hash
1117
+ })).sort((a, b) => String(a.id).localeCompare(String(b.id)));
1118
+ const [analysis] = await this.insert(db, "intake_analyses", {
1119
+ item_id: itemId,
1120
+ revision,
1121
+ fence: 0,
1122
+ attempt_number: 0,
1123
+ current_attempt_id: null,
1124
+ state: "queued",
1125
+ input_digest: digest({
1126
+ inputs,
1127
+ configuration
1128
+ }),
1129
+ data: canonical({
1130
+ inputs,
1131
+ configuration,
1132
+ requestKey,
1133
+ configDigest
1134
+ })
1121
1135
  });
1136
+ await db.query("UPDATE intake_items SET analysis_revision=?,processing_state='queued' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", revision, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1137
+ await this.insert(db, "intake_dispatches", {
1138
+ item_id: itemId,
1139
+ stage: "analyze",
1140
+ revision,
1141
+ state: "pending",
1142
+ job_id: "",
1143
+ deliveries: 0,
1144
+ data: "{}"
1145
+ }, "tenant_id,item_id,stage,revision");
1146
+ return {
1147
+ id: String(analysis.id),
1148
+ revision
1149
+ };
1150
+ }
1151
+ /** Record an authenticated split correction and queue fresh extraction, never copy model output. */
1152
+ async reviseLogicalSplit(input) {
1153
+ const frozen = structuredClone(input);
1154
+ nonempty(frozen.requestId);
1155
+ integer(frozen.expectedRevision);
1156
+ if (!Array.isArray(frozen.groups) || !frozen.groups.length || frozen.groups.length > 100 || frozen.groups.some((group) => !Array.isArray(group) || !group.length || group.length > 1e3 || group.some((page) => !Number.isSafeInteger(page) || page < 1))) throw new Error("Invalid logical split");
1157
+ const pages = frozen.groups.flat();
1158
+ if (pages.length > 1e3 || new Set(pages).size !== pages.length) throw new Error("Invalid logical split");
1159
+ const requestKey = `split:${frozen.requestId}`;
1160
+ const correction = {
1161
+ ...frozen,
1162
+ actorId: this.scope.actorId,
1163
+ kind: "logical_split"
1164
+ };
1165
+ const replay = await this.tx(async (db) => {
1166
+ await this.lock(db, frozen.itemId);
1167
+ await this.item(frozen.itemId, "process", db);
1168
+ const found = (await this.rows(db, "intake_analyses", "item_id=?", [frozen.itemId])).find((row) => object(row.data).requestKey === requestKey);
1169
+ if (!found) return null;
1170
+ if (digest(object(object(found.data).configuration).humanCorrection) !== digest(correction)) throw new Error("Split request conflict");
1171
+ return {
1172
+ id: String(found.id),
1173
+ revision: Number(found.revision)
1174
+ };
1175
+ });
1176
+ if (replay) {
1177
+ await (await this.proposals())[generationSnapshotRead](frozen.itemId, (work) => this.tx(work), (db) => this.completedAnalysisSnapshot(db, frozen.itemId, frozen.attemptId, false), void 0, true);
1178
+ return replay;
1179
+ }
1180
+ let result;
1181
+ await (await this.proposals())[generationSnapshotRead](frozen.itemId, (work) => this.tx(work), (db) => this.completedAnalysisSnapshot(db, frozen.itemId, frozen.attemptId, true), async (db, current) => {
1182
+ if (current.revision !== frozen.expectedRevision) throw new Error("Stale logical split");
1183
+ const pin = object(object(current.configuration.proposals).source);
1184
+ const source = await this.completedAnalysisSnapshot(db, frozen.itemId, String(pin.attemptId), false);
1185
+ for (const key of [
1186
+ "attemptId",
1187
+ "revision",
1188
+ "inputDigest",
1189
+ "outputDigest",
1190
+ "evidenceDigest"
1191
+ ]) if (pin[key] !== source[key]) throw new Error("Split source changed");
1192
+ const extraction = source.result.output.results;
1193
+ if (!Array.isArray(extraction)) throw new Error("Split source unavailable");
1194
+ const evidence = extraction.find((value) => object(object(value).evidence).id === frozen.evidenceId);
1195
+ if (!evidence) throw new Error("Split evidence unavailable");
1196
+ const observed = /* @__PURE__ */ new Set();
1197
+ for (const value of object(evidence).segments) {
1198
+ const location = object(object(value).location);
1199
+ if (location.kind === "page") observed.add(Number(location.page));
1200
+ if (location.kind === "pages") {
1201
+ const from = Number(location.startPage), to = Number(location.endPage);
1202
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || to - from > 1e3) throw new Error("Invalid source range");
1203
+ for (let page = from; page <= to; page++) observed.add(page);
1204
+ }
1205
+ }
1206
+ if (!observed.size || pages.length !== observed.size || pages.some((page) => !observed.has(page))) throw new Error("Split must cover observed pages exactly");
1207
+ result = await this.appendAnalysis(db, frozen.itemId, {
1208
+ ...source.configuration,
1209
+ humanCorrection: correction
1210
+ }, requestKey);
1211
+ }, true);
1212
+ if (!result) throw new Error("Split unavailable");
1213
+ return result;
1122
1214
  }
1123
1215
  /** Allocate a fenced attempt; active leases cannot be stolen before expiry. */
1124
1216
  async claimAnalysis(itemId, revision, workerId) {
@@ -1259,7 +1351,7 @@ var IngestionService = class {
1259
1351
  "outputDigest",
1260
1352
  "evidenceDigest"
1261
1353
  ]) 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) => ({
1354
+ if (source.result.provider !== "smrt-ingestion-extraction" || source.configuration.stage === "interpret" || source.revision !== generation.revision - 1 || digest(configuration.humanCorrection ?? null) !== digest(source.configuration.humanCorrection ?? null) || digest(generation.evidence.map((entry) => ({
1263
1355
  id: entry.id,
1264
1356
  hash: entry.contentHash
1265
1357
  }))) !== source.evidenceDigest) throw new Error("Generation evidence changed");
@@ -1583,6 +1675,6 @@ var IngestionService = class {
1583
1675
  }
1584
1676
  };
1585
1677
  //#endregion
1586
- export { EmailSourceAdapter, IngestionService, WatchFolderSourceAdapter, createSDKExtractionAdapter, createSourceDeliveryHandler, createVendorWebhookHandler, extractAnalysis, proposeDocumentSplits };
1678
+ export { EmailSourceAdapter, GenerationSnapshotStaleError, IngestionService, WatchFolderSourceAdapter, createSDKExtractionAdapter, createSourceDeliveryHandler, createVendorWebhookHandler, extractAnalysis, proposeDocumentSplits };
1587
1679
 
1588
1680
  //# sourceMappingURL=server.js.map