hazo_collab_forms 3.1.7 → 5.0.2

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 (284) hide show
  1. package/CHANGE_LOG.md +207 -0
  2. package/README.md +3 -0
  3. package/dist/components/clarification/clarification_item_body.d.ts +19 -1
  4. package/dist/components/clarification/clarification_item_body.d.ts.map +1 -1
  5. package/dist/components/clarification/clarification_item_body.js +114 -6
  6. package/dist/components/clarification/clarification_item_body.js.map +1 -1
  7. package/dist/components/clarification/clarification_thread.js +1 -1
  8. package/dist/components/clarification/clarification_thread.js.map +1 -1
  9. package/dist/components/clarification/index.d.ts +2 -0
  10. package/dist/components/clarification/index.d.ts.map +1 -1
  11. package/dist/components/clarification/index.js +1 -0
  12. package/dist/components/clarification/index.js.map +1 -1
  13. package/dist/components/clarification/resolution_status_strip.d.ts +18 -0
  14. package/dist/components/clarification/resolution_status_strip.d.ts.map +1 -0
  15. package/dist/components/clarification/resolution_status_strip.js +20 -0
  16. package/dist/components/clarification/resolution_status_strip.js.map +1 -0
  17. package/dist/components/hazo_fb_form/context.d.ts +1 -1
  18. package/dist/components/hazo_fb_form/context.d.ts.map +1 -1
  19. package/dist/components/hazo_fb_form/hazo_fb_form.d.ts.map +1 -1
  20. package/dist/components/hazo_fb_form/hazo_fb_form.js +330 -113
  21. package/dist/components/hazo_fb_form/hazo_fb_form.js.map +1 -1
  22. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts +3 -3
  23. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts.map +1 -1
  24. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js +339 -46
  25. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js.map +1 -1
  26. package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts +3 -1
  27. package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts.map +1 -1
  28. package/dist/components/hazo_fb_form/hooks/use_llm_run.js +89 -11
  29. package/dist/components/hazo_fb_form/hooks/use_llm_run.js.map +1 -1
  30. package/dist/components/hazo_fb_form/shared/agent_stepper.js +1 -1
  31. package/dist/components/hazo_fb_form/shared/agent_stepper.js.map +1 -1
  32. package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts +9 -0
  33. package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts.map +1 -0
  34. package/dist/components/hazo_fb_form/shared/file_status_accordion.js +39 -0
  35. package/dist/components/hazo_fb_form/shared/file_status_accordion.js.map +1 -0
  36. package/dist/components/hazo_fb_form/shared/format.d.ts.map +1 -1
  37. package/dist/components/hazo_fb_form/shared/format.js +8 -3
  38. package/dist/components/hazo_fb_form/shared/format.js.map +1 -1
  39. package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts +7 -1
  40. package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts.map +1 -1
  41. package/dist/components/hazo_fb_form/shared/send_back_item_card.js +6 -3
  42. package/dist/components/hazo_fb_form/shared/send_back_item_card.js.map +1 -1
  43. package/dist/components/hazo_fb_form/types.d.ts +3 -1
  44. package/dist/components/hazo_fb_form/types.d.ts.map +1 -1
  45. package/dist/components/hazo_fb_form/views/back_office_view.js +1 -1
  46. package/dist/components/hazo_fb_form/views/back_office_view.js.map +1 -1
  47. package/dist/components/hazo_fb_form/views/clarifications_view.js +2 -2
  48. package/dist/components/hazo_fb_form/views/clarifications_view.js.map +1 -1
  49. package/dist/components/hazo_fb_form/views/front_office_view.d.ts.map +1 -1
  50. package/dist/components/hazo_fb_form/views/front_office_view.js +62 -41
  51. package/dist/components/hazo_fb_form/views/front_office_view.js.map +1 -1
  52. package/dist/components/hazo_fb_form/views/interim_view.js +3 -3
  53. package/dist/components/hazo_fb_form/views/interim_view.js.map +1 -1
  54. package/dist/components/hazo_fb_form/views/review_queue_view.d.ts.map +1 -1
  55. package/dist/components/hazo_fb_form/views/review_queue_view.js +22 -9
  56. package/dist/components/hazo_fb_form/views/review_queue_view.js.map +1 -1
  57. package/dist/components/hazo_validation_rule_editor/components/rule_editor.d.ts.map +1 -1
  58. package/dist/components/hazo_validation_rule_editor/components/rule_editor.js +32 -3
  59. package/dist/components/hazo_validation_rule_editor/components/rule_editor.js.map +1 -1
  60. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts +20 -0
  61. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts.map +1 -0
  62. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js +34 -0
  63. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js.map +1 -0
  64. package/dist/components/hazo_validation_rule_editor/context.d.ts +3 -2
  65. package/dist/components/hazo_validation_rule_editor/context.d.ts.map +1 -1
  66. package/dist/components/hazo_validation_rule_editor/context.js +15 -3
  67. package/dist/components/hazo_validation_rule_editor/context.js.map +1 -1
  68. package/dist/components/hazo_validation_rule_editor/types.d.ts +7 -1
  69. package/dist/components/hazo_validation_rule_editor/types.d.ts.map +1 -1
  70. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts +1 -1
  71. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts.map +1 -1
  72. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js +2 -2
  73. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js.map +1 -1
  74. package/dist/components/index.d.ts +2 -0
  75. package/dist/components/index.d.ts.map +1 -1
  76. package/dist/components/index.js +2 -0
  77. package/dist/components/index.js.map +1 -1
  78. package/dist/components/shared/document_type_editor.d.ts +31 -0
  79. package/dist/components/shared/document_type_editor.d.ts.map +1 -0
  80. package/dist/components/shared/document_type_editor.js +60 -0
  81. package/dist/components/shared/document_type_editor.js.map +1 -0
  82. package/dist/components/shared/file_bar/file_bar.d.ts +7 -1
  83. package/dist/components/shared/file_bar/file_bar.d.ts.map +1 -1
  84. package/dist/components/shared/file_bar/file_bar.js +5 -3
  85. package/dist/components/shared/file_bar/file_bar.js.map +1 -1
  86. package/dist/components/shared/file_bar/file_bar_validation_dialog.js +4 -4
  87. package/dist/components/shared/file_bar/file_bar_validation_dialog.js.map +1 -1
  88. package/dist/components/shared/file_status_icon.d.ts +23 -0
  89. package/dist/components/shared/file_status_icon.d.ts.map +1 -0
  90. package/dist/components/shared/file_status_icon.js +38 -0
  91. package/dist/components/shared/file_status_icon.js.map +1 -0
  92. package/dist/components/shared/json_data_panel/json_data_panel.d.ts +1 -1
  93. package/dist/components/shared/json_data_panel/json_data_panel.d.ts.map +1 -1
  94. package/dist/components/shared/json_data_panel/json_data_panel.js +27 -2
  95. package/dist/components/shared/json_data_panel/json_data_panel.js.map +1 -1
  96. package/dist/components/shared/rule_result_card.d.ts.map +1 -1
  97. package/dist/components/shared/rule_result_card.js +5 -4
  98. package/dist/components/shared/rule_result_card.js.map +1 -1
  99. package/dist/components/thread_form/components/add_question_dialog.d.ts +12 -0
  100. package/dist/components/thread_form/components/add_question_dialog.d.ts.map +1 -0
  101. package/dist/components/thread_form/components/add_question_dialog.js +36 -0
  102. package/dist/components/thread_form/components/add_question_dialog.js.map +1 -0
  103. package/dist/components/thread_form/components/agent_compose_dialog.d.ts +30 -0
  104. package/dist/components/thread_form/components/agent_compose_dialog.d.ts.map +1 -0
  105. package/dist/components/thread_form/components/agent_compose_dialog.js +45 -0
  106. package/dist/components/thread_form/components/agent_compose_dialog.js.map +1 -0
  107. package/dist/components/thread_form/components/clarification.d.ts +14 -0
  108. package/dist/components/thread_form/components/clarification.d.ts.map +1 -0
  109. package/dist/components/thread_form/components/clarification.js +12 -0
  110. package/dist/components/thread_form/components/clarification.js.map +1 -0
  111. package/dist/components/thread_form/components/collected_data_view.d.ts +15 -0
  112. package/dist/components/thread_form/components/collected_data_view.d.ts.map +1 -0
  113. package/dist/components/thread_form/components/collected_data_view.js +121 -0
  114. package/dist/components/thread_form/components/collected_data_view.js.map +1 -0
  115. package/dist/components/thread_form/components/coverage_card.d.ts +11 -0
  116. package/dist/components/thread_form/components/coverage_card.d.ts.map +1 -0
  117. package/dist/components/thread_form/components/coverage_card.js +60 -0
  118. package/dist/components/thread_form/components/coverage_card.js.map +1 -0
  119. package/dist/components/thread_form/components/file_bar.d.ts +93 -0
  120. package/dist/components/thread_form/components/file_bar.d.ts.map +1 -0
  121. package/dist/components/thread_form/components/file_bar.js +251 -0
  122. package/dist/components/thread_form/components/file_bar.js.map +1 -0
  123. package/dist/components/thread_form/components/file_info_dialog.d.ts +15 -0
  124. package/dist/components/thread_form/components/file_info_dialog.d.ts.map +1 -0
  125. package/dist/components/thread_form/components/file_info_dialog.js +64 -0
  126. package/dist/components/thread_form/components/file_info_dialog.js.map +1 -0
  127. package/dist/components/thread_form/components/issue_group_tree.d.ts +20 -0
  128. package/dist/components/thread_form/components/issue_group_tree.d.ts.map +1 -0
  129. package/dist/components/thread_form/components/issue_group_tree.js +164 -0
  130. package/dist/components/thread_form/components/issue_group_tree.js.map +1 -0
  131. package/dist/components/thread_form/components/pdf_side_panel.d.ts +20 -0
  132. package/dist/components/thread_form/components/pdf_side_panel.d.ts.map +1 -0
  133. package/dist/components/thread_form/components/pdf_side_panel.js +63 -0
  134. package/dist/components/thread_form/components/pdf_side_panel.js.map +1 -0
  135. package/dist/components/thread_form/components/rule_decision_row.d.ts +31 -0
  136. package/dist/components/thread_form/components/rule_decision_row.d.ts.map +1 -0
  137. package/dist/components/thread_form/components/rule_decision_row.js +20 -0
  138. package/dist/components/thread_form/components/rule_decision_row.js.map +1 -0
  139. package/dist/components/thread_form/components/send_back_message.d.ts +32 -0
  140. package/dist/components/thread_form/components/send_back_message.d.ts.map +1 -0
  141. package/dist/components/thread_form/components/send_back_message.js +82 -0
  142. package/dist/components/thread_form/components/send_back_message.js.map +1 -0
  143. package/dist/components/thread_form/components/shared.d.ts +54 -0
  144. package/dist/components/thread_form/components/shared.d.ts.map +1 -0
  145. package/dist/components/thread_form/components/shared.js +136 -0
  146. package/dist/components/thread_form/components/shared.js.map +1 -0
  147. package/dist/components/thread_form/components/task_card.d.ts +90 -0
  148. package/dist/components/thread_form/components/task_card.d.ts.map +1 -0
  149. package/dist/components/thread_form/components/task_card.js +63 -0
  150. package/dist/components/thread_form/components/task_card.js.map +1 -0
  151. package/dist/components/thread_form/components/text_doc_check.d.ts +15 -0
  152. package/dist/components/thread_form/components/text_doc_check.d.ts.map +1 -0
  153. package/dist/components/thread_form/components/text_doc_check.js +16 -0
  154. package/dist/components/thread_form/components/text_doc_check.js.map +1 -0
  155. package/dist/components/thread_form/components/text_extraction.d.ts +14 -0
  156. package/dist/components/thread_form/components/text_extraction.d.ts.map +1 -0
  157. package/dist/components/thread_form/components/text_extraction.js +16 -0
  158. package/dist/components/thread_form/components/text_extraction.js.map +1 -0
  159. package/dist/components/thread_form/components/thread_composer.d.ts +15 -0
  160. package/dist/components/thread_form/components/thread_composer.d.ts.map +1 -0
  161. package/dist/components/thread_form/components/thread_composer.js +93 -0
  162. package/dist/components/thread_form/components/thread_composer.js.map +1 -0
  163. package/dist/components/thread_form/components/thread_timeline.d.ts +65 -0
  164. package/dist/components/thread_form/components/thread_timeline.d.ts.map +1 -0
  165. package/dist/components/thread_form/components/thread_timeline.js +225 -0
  166. package/dist/components/thread_form/components/thread_timeline.js.map +1 -0
  167. package/dist/components/thread_form/hooks/use_file_pipeline.d.ts +126 -0
  168. package/dist/components/thread_form/hooks/use_file_pipeline.d.ts.map +1 -0
  169. package/dist/components/thread_form/hooks/use_file_pipeline.js +760 -0
  170. package/dist/components/thread_form/hooks/use_file_pipeline.js.map +1 -0
  171. package/dist/components/thread_form/hooks/use_thread_form.d.ts +36 -0
  172. package/dist/components/thread_form/hooks/use_thread_form.d.ts.map +1 -0
  173. package/dist/components/thread_form/hooks/use_thread_form.js +126 -0
  174. package/dist/components/thread_form/hooks/use_thread_form.js.map +1 -0
  175. package/dist/components/thread_form/index.d.ts +33 -0
  176. package/dist/components/thread_form/index.d.ts.map +1 -0
  177. package/dist/components/thread_form/index.js +30 -0
  178. package/dist/components/thread_form/index.js.map +1 -0
  179. package/dist/components/thread_form/sample_data.d.ts +8 -0
  180. package/dist/components/thread_form/sample_data.d.ts.map +1 -0
  181. package/dist/components/thread_form/sample_data.js +658 -0
  182. package/dist/components/thread_form/sample_data.js.map +1 -0
  183. package/dist/components/thread_form/thread_form.d.ts +7 -0
  184. package/dist/components/thread_form/thread_form.d.ts.map +1 -0
  185. package/dist/components/thread_form/thread_form.js +1385 -0
  186. package/dist/components/thread_form/thread_form.js.map +1 -0
  187. package/dist/components/thread_form/types.d.ts +402 -0
  188. package/dist/components/thread_form/types.d.ts.map +1 -0
  189. package/dist/components/thread_form/types.js +23 -0
  190. package/dist/components/thread_form/types.js.map +1 -0
  191. package/dist/components/thread_form/utils/file_decision_state.d.ts +22 -0
  192. package/dist/components/thread_form/utils/file_decision_state.d.ts.map +1 -0
  193. package/dist/components/thread_form/utils/file_decision_state.js +37 -0
  194. package/dist/components/thread_form/utils/file_decision_state.js.map +1 -0
  195. package/dist/components/thread_form/utils/merge_send_back.d.ts +13 -0
  196. package/dist/components/thread_form/utils/merge_send_back.d.ts.map +1 -0
  197. package/dist/components/thread_form/utils/merge_send_back.js +23 -0
  198. package/dist/components/thread_form/utils/merge_send_back.js.map +1 -0
  199. package/dist/lib/autofill_handler.d.ts.map +1 -1
  200. package/dist/lib/autofill_handler.js +5 -44
  201. package/dist/lib/autofill_handler.js.map +1 -1
  202. package/dist/lib/classification_handler.d.ts +105 -0
  203. package/dist/lib/classification_handler.d.ts.map +1 -0
  204. package/dist/lib/classification_handler.js +342 -0
  205. package/dist/lib/classification_handler.js.map +1 -0
  206. package/dist/lib/content_gate_handler.d.ts +37 -0
  207. package/dist/lib/content_gate_handler.d.ts.map +1 -0
  208. package/dist/lib/content_gate_handler.js +126 -0
  209. package/dist/lib/content_gate_handler.js.map +1 -0
  210. package/dist/lib/index.d.ts +10 -0
  211. package/dist/lib/index.d.ts.map +1 -1
  212. package/dist/lib/index.js +5 -0
  213. package/dist/lib/index.js.map +1 -1
  214. package/dist/lib/periodic_coverage_runner.d.ts +24 -0
  215. package/dist/lib/periodic_coverage_runner.d.ts.map +1 -0
  216. package/dist/lib/periodic_coverage_runner.js +121 -0
  217. package/dist/lib/periodic_coverage_runner.js.map +1 -0
  218. package/dist/lib/resolution_handler.d.ts +150 -0
  219. package/dist/lib/resolution_handler.d.ts.map +1 -0
  220. package/dist/lib/resolution_handler.js +597 -0
  221. package/dist/lib/resolution_handler.js.map +1 -0
  222. package/dist/lib/resolve_variable.d.ts +25 -0
  223. package/dist/lib/resolve_variable.d.ts.map +1 -0
  224. package/dist/lib/resolve_variable.js +77 -0
  225. package/dist/lib/resolve_variable.js.map +1 -0
  226. package/dist/lib/validation_handler.d.ts +27 -3
  227. package/dist/lib/validation_handler.d.ts.map +1 -1
  228. package/dist/lib/validation_handler.js +338 -288
  229. package/dist/lib/validation_handler.js.map +1 -1
  230. package/dist/types/clarification.d.ts +54 -0
  231. package/dist/types/clarification.d.ts.map +1 -1
  232. package/dist/types/fb_form_data.d.ts +273 -123
  233. package/dist/types/fb_form_data.d.ts.map +1 -1
  234. package/dist/types/fb_form_data.js +44 -58
  235. package/dist/types/fb_form_data.js.map +1 -1
  236. package/dist/types/fb_form_data_v1.d.ts +250 -0
  237. package/dist/types/fb_form_data_v1.d.ts.map +1 -0
  238. package/dist/types/fb_form_data_v1.js +117 -0
  239. package/dist/types/fb_form_data_v1.js.map +1 -0
  240. package/dist/types/fb_form_instance.d.ts +1 -1
  241. package/dist/types/fb_form_instance.d.ts.map +1 -1
  242. package/dist/types/index.d.ts +5 -3
  243. package/dist/types/index.d.ts.map +1 -1
  244. package/dist/types/index.js +2 -1
  245. package/dist/types/index.js.map +1 -1
  246. package/dist/types/validation.d.ts +134 -12
  247. package/dist/types/validation.d.ts.map +1 -1
  248. package/dist/utils/expectation_extractor.d.ts +31 -0
  249. package/dist/utils/expectation_extractor.d.ts.map +1 -0
  250. package/dist/utils/expectation_extractor.js +142 -0
  251. package/dist/utils/expectation_extractor.js.map +1 -0
  252. package/dist/utils/fb_data_adapter.d.ts +7 -2
  253. package/dist/utils/fb_data_adapter.d.ts.map +1 -1
  254. package/dist/utils/fb_data_adapter.js +58 -7
  255. package/dist/utils/fb_data_adapter.js.map +1 -1
  256. package/dist/utils/fb_data_adapter_v2.d.ts +17 -0
  257. package/dist/utils/fb_data_adapter_v2.d.ts.map +1 -0
  258. package/dist/utils/fb_data_adapter_v2.js +483 -0
  259. package/dist/utils/fb_data_adapter_v2.js.map +1 -0
  260. package/dist/utils/fb_data_helpers.d.ts +1 -1
  261. package/dist/utils/fb_data_helpers.d.ts.map +1 -1
  262. package/dist/utils/fb_data_mutations.d.ts +1 -1
  263. package/dist/utils/fb_data_mutations.d.ts.map +1 -1
  264. package/dist/utils/fb_data_mutations_v2.d.ts +46 -0
  265. package/dist/utils/fb_data_mutations_v2.d.ts.map +1 -0
  266. package/dist/utils/fb_data_mutations_v2.js +341 -0
  267. package/dist/utils/fb_data_mutations_v2.js.map +1 -0
  268. package/dist/utils/fb_data_queries.d.ts +81 -0
  269. package/dist/utils/fb_data_queries.d.ts.map +1 -0
  270. package/dist/utils/fb_data_queries.js +354 -0
  271. package/dist/utils/fb_data_queries.js.map +1 -0
  272. package/dist/utils/index.d.ts +4 -0
  273. package/dist/utils/index.d.ts.map +1 -1
  274. package/dist/utils/index.js +6 -0
  275. package/dist/utils/index.js.map +1 -1
  276. package/dist/utils/issue_bucketing.d.ts +36 -0
  277. package/dist/utils/issue_bucketing.d.ts.map +1 -0
  278. package/dist/utils/issue_bucketing.js +107 -0
  279. package/dist/utils/issue_bucketing.js.map +1 -0
  280. package/dist/utils/validation_result.d.ts +32 -0
  281. package/dist/utils/validation_result.d.ts.map +1 -0
  282. package/dist/utils/validation_result.js +55 -0
  283. package/dist/utils/validation_result.js.map +1 -0
  284. package/package.json +16 -4
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Server-side validation route handler factory.
3
3
  *
4
- * Creates a POST handler that executes validation rules against uploaded documents
5
- * using LLM analysis, and returns ClarificationItems for any issues found.
4
+ * Creates a POST handler that executes validation rules against uploaded
5
+ * documents or plain text using LLM analysis, and returns ClarificationItems
6
+ * for any issues found.
6
7
  *
7
8
  * Usage:
8
9
  * import { create_validation_route } from 'hazo_collab_forms/lib';
@@ -13,112 +14,145 @@
13
14
  */
14
15
  import 'server-only';
15
16
  import { DEFAULT_CLARIFICATION_TEMPLATES } from '../config/clarification_templates.js';
16
- import { DEFAULT_VALIDATION_PROMPT_SUFFIX } from '../config/defaults.js';
17
17
  import { get_config } from './config.js';
18
+ import { make_issue_id } from '../utils/validation_result.js';
19
+ import { extract_expectation } from '../utils/expectation_extractor.js';
18
20
  // ============================================================================
19
21
  // Helpers
20
22
  // ============================================================================
21
23
  function generate_clarification_id() {
22
24
  return `clr_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
23
25
  }
24
- /**
25
- * Extract has_issue and issue_description from a parsed JSON object.
26
- * Supports both new format (validation_passed, validation_details) and
27
- * legacy format (has_issue, issue_description).
28
- */
29
- function extract_validation_fields(parsed) {
30
- // Determine has_issue: prefer new `validation_passed` (inverted), fall back to legacy `has_issue`
31
- const has_issue = 'validation_passed' in parsed
32
- ? !parsed.validation_passed
33
- : !!parsed.has_issue;
34
- // Determine issue_description: prefer new `validation_details`, fall back to legacy names
35
- const issue_description = parsed.validation_details ||
36
- parsed.issue_description ||
37
- parsed.description ||
38
- parsed.issue ||
39
- undefined;
40
- return {
41
- has_issue,
42
- issue_description,
43
- confidence: typeof parsed.confidence === 'number' ? parsed.confidence : undefined,
44
- };
45
- }
46
- /**
47
- * Parse LLM response for validation results.
48
- * Supports new format: { validation_passed, validation_details, confidence }
49
- * and legacy format: { has_issue, issue_description, confidence }
50
- */
51
- function parse_validation_response(text) {
52
- const cleaned = text.trim();
53
- // Try direct parse
54
- try {
55
- const parsed = JSON.parse(cleaned);
56
- return extract_validation_fields(parsed);
57
- }
58
- catch {
59
- // Continue
60
- }
61
- // Strip markdown code fences
62
- const fence_match = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
63
- if (fence_match) {
64
- try {
65
- const parsed = JSON.parse(fence_match[1].trim());
66
- return extract_validation_fields(parsed);
67
- }
68
- catch {
69
- // Continue
70
- }
71
- }
72
- // Try to find JSON in text
73
- const json_match = cleaned.match(/\{[\s\S]*\}/);
74
- if (json_match) {
75
- try {
76
- const parsed = JSON.parse(json_match[0]);
77
- return extract_validation_fields(parsed);
78
- }
79
- catch {
80
- // Continue
81
- }
82
- }
83
- // Heuristic: if the text mentions "issue", "problem", "invalid", etc., consider it an issue
84
- const issue_keywords = ['issue', 'problem', 'invalid', 'missing', 'incomplete', 'error', 'incorrect'];
85
- const lower = cleaned.toLowerCase();
86
- const has_issue = issue_keywords.some(k => lower.includes(k));
87
- return {
88
- has_issue,
89
- issue_description: has_issue ? cleaned.slice(0, 500) : undefined,
90
- };
91
- }
92
26
  function is_image_mime(mime_type) {
93
27
  return mime_type.startsWith('image/');
94
28
  }
95
29
  function is_document_mime(mime_type) {
96
30
  return mime_type === 'application/pdf';
97
31
  }
98
- /**
99
- * Substitute {{variable}} placeholders in prompt text.
100
- */
32
+ /** Substitute {{variable}} placeholders in prompt text. */
101
33
  function substitute_variables(prompt, variables) {
102
34
  return prompt.replace(/\{\{(\w+)\}\}/g, (_, key) => {
103
35
  return variables[key] ?? `{{${key}}}`;
104
36
  });
105
37
  }
106
- /**
107
- * Build a validation prompt that wraps the rule prompt with a configurable response format suffix.
108
- * Reads `[validation] prompt_suffix` from INI config, falling back to DEFAULT_VALIDATION_PROMPT_SUFFIX.
109
- */
110
- function build_validation_prompt(rule_prompt) {
111
- const suffix = get_config('validation', 'prompt_suffix') ?? DEFAULT_VALIDATION_PROMPT_SUFFIX;
112
- return `You are a document validation assistant. Analyze the provided document according to the following rule and determine if there is an issue.
113
-
114
- VALIDATION RULE:
115
- ${rule_prompt}
116
-
117
- ${suffix}`;
38
+ /** Wrap a per-rule prompt with the response envelope from hazo_prompts. */
39
+ function wrap_rule_prompt(rule_prompt, wrapper) {
40
+ return substitute_variables(wrapper, { rule_prompt });
118
41
  }
42
+ // ============================================================================
43
+ // Parser (unified — v4)
44
+ // ============================================================================
119
45
  /**
120
- * Create a ClarificationItem from a validation rule result.
46
+ * Parse the LLM response into a ValidationRuleResult.
47
+ *
48
+ * Expected shape (per validation/response_wrapper in hazo_prompts):
49
+ * {
50
+ * "summary": "...", // optional rule-level explanation
51
+ * "issues": [ // empty = passed
52
+ * { "description": "...", "amount": "...", "date": "...", "reason": "...",
53
+ * "client_comment": "...", "confidence": 0.9 }
54
+ * ],
55
+ * "confidence": 0.95 // optional rule-level confidence
56
+ * }
57
+ *
58
+ * Tolerates: direct JSON, fenced code block, regex-extracted object, and
59
+ * top-level arrays (treated as the issues array).
121
60
  */
61
+ export function parse_response(text, rule_id) {
62
+ const cleaned = text.trim();
63
+ const try_parse = (raw) => {
64
+ try {
65
+ return JSON.parse(raw);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ };
71
+ const parsed = try_parse(cleaned) ??
72
+ (() => {
73
+ const fence = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
74
+ return fence ? try_parse(fence[1].trim()) : null;
75
+ })() ??
76
+ (() => {
77
+ const obj = cleaned.match(/\{[\s\S]*\}/);
78
+ if (obj)
79
+ return try_parse(obj[0]);
80
+ const arr = cleaned.match(/\[[\s\S]*\]/);
81
+ return arr ? try_parse(arr[0]) : null;
82
+ })();
83
+ if (!parsed)
84
+ return { issues: [] };
85
+ let raw_issues = [];
86
+ let summary;
87
+ let confidence;
88
+ let extracted_data;
89
+ if (Array.isArray(parsed)) {
90
+ raw_issues = parsed;
91
+ }
92
+ else if (typeof parsed === 'object' && parsed !== null) {
93
+ const obj = parsed;
94
+ if (Array.isArray(obj.issues))
95
+ raw_issues = obj.issues;
96
+ else if (Array.isArray(obj.items))
97
+ raw_issues = obj.items;
98
+ if (typeof obj.summary === 'string')
99
+ summary = obj.summary;
100
+ if (typeof obj.confidence === 'number')
101
+ confidence = obj.confidence;
102
+ if (obj.extracted_data && typeof obj.extracted_data === 'object' && !Array.isArray(obj.extracted_data)) {
103
+ extracted_data = obj.extracted_data;
104
+ }
105
+ }
106
+ const issues = [];
107
+ for (const row of raw_issues) {
108
+ if (typeof row !== 'object' || row === null)
109
+ continue;
110
+ const r = row;
111
+ const description_raw = r.description ||
112
+ r.issue_description ||
113
+ r.issue ||
114
+ r.label ||
115
+ '';
116
+ if (!description_raw)
117
+ continue;
118
+ const amount = r.amount;
119
+ const date = r.date;
120
+ const reason = r.reason;
121
+ const client_comment = r.client_comment;
122
+ const issue_confidence = typeof r.confidence === 'number' ? r.confidence : undefined;
123
+ // Compose a richer human-readable description that includes amount/date
124
+ // so list views show useful context without reaching into sub-fields.
125
+ const parts = [description_raw];
126
+ if (amount !== undefined && amount !== '')
127
+ parts.push(`$${amount}`);
128
+ if (date)
129
+ parts.push(String(date));
130
+ const header = parts.join(' — ');
131
+ const issue_description = reason && reason !== description_raw
132
+ ? `${header}. ${reason}`
133
+ : header;
134
+ const id_seed = [rule_id, description_raw, amount, date];
135
+ const fallback = `iss-${issues.length}`;
136
+ issues.push({
137
+ issue_id: make_issue_id(id_seed, fallback),
138
+ issue_description,
139
+ ...(amount !== undefined && amount !== '' ? { amount: String(amount) } : {}),
140
+ ...(date ? { date: String(date) } : {}),
141
+ ...(reason ? { reason } : {}),
142
+ ...(client_comment ? { client_comment } : {}),
143
+ ...(issue_confidence !== undefined ? { confidence: issue_confidence } : {}),
144
+ });
145
+ }
146
+ return {
147
+ issues,
148
+ ...(summary ? { summary } : {}),
149
+ ...(confidence !== undefined ? { confidence } : {}),
150
+ ...(extracted_data ? { extracted_data } : {}),
151
+ };
152
+ }
153
+ // ============================================================================
154
+ // Clarification creation
155
+ // ============================================================================
122
156
  function create_clarification_from_result(rule, result, doc_info) {
123
157
  const template = rule.clarification_type !== 'none'
124
158
  ? DEFAULT_CLARIFICATION_TEMPLATES[rule.clarification_type]
@@ -126,6 +160,16 @@ function create_clarification_from_result(rule, result, doc_info) {
126
160
  const response_options = rule.custom_response_options ??
127
161
  template?.response_options ??
128
162
  [];
163
+ // Derive a human-readable issue description: prefer the first issue's
164
+ // client_comment (LLM-drafted for the client), fall back to its description,
165
+ // then the rule-level summary, then template/default.
166
+ const first = result.issues[0];
167
+ const derived_issue_description = first?.client_comment ??
168
+ first?.issue_description ??
169
+ result.summary ??
170
+ template?.default_issue_description ??
171
+ 'A validation issue was detected.';
172
+ const expectation = first ? extract_expectation(first) : undefined;
129
173
  return {
130
174
  id: generate_clarification_id(),
131
175
  type: rule.clarification_type,
@@ -135,10 +179,8 @@ function create_clarification_from_result(rule, result, doc_info) {
135
179
  rule_name: rule.name,
136
180
  rule_id: rule.rule_id,
137
181
  issue_description: rule.custom_issue_description ??
138
- result.issue_description ??
139
- template?.default_issue_description ??
140
- 'A validation issue was detected.',
141
- validation_details: result.issue_description,
182
+ derived_issue_description,
183
+ validation_details: result.summary ?? first?.issue_description,
142
184
  doc_references: doc_info ? [{
143
185
  file_id: '', // Client enriches with actual file_id
144
186
  file_name: doc_info.file_name,
@@ -149,6 +191,7 @@ function create_clarification_from_result(rule, result, doc_info) {
149
191
  response_options,
150
192
  response_files: [],
151
193
  created_at: new Date().toISOString(),
194
+ ...(expectation ? { expectation } : {}),
152
195
  };
153
196
  }
154
197
  // ============================================================================
@@ -171,6 +214,51 @@ export function create_validation_route(options) {
171
214
  // Cache the dynamic import
172
215
  let llm_api_module = null;
173
216
  let init_promise = null;
217
+ // Validation wrappers live in hazo_prompts (not in code). Loaded once per
218
+ // route-handler lifetime and cached. Updates via the Prompt Editor require a
219
+ // server restart to pick up — same behaviour as every other prompt loaded
220
+ // here. There is intentionally no hardcoded fallback: missing wrappers must
221
+ // surface as a 500 with a seeding hint.
222
+ let wrapper_cache = null;
223
+ async function get_validation_wrappers() {
224
+ if (wrapper_cache)
225
+ return wrapper_cache;
226
+ await ensure_initialized();
227
+ const api = await get_llm_api();
228
+ const connect = api.get_hazo_connect?.() ?? null;
229
+ if (!connect) {
230
+ throw new Error('hazo_llm_api hazo_connect is not available — cannot load validation wrappers from hazo_prompts');
231
+ }
232
+ const fetch_one = async (key) => {
233
+ const r = await connect.get_by_area_key('validation', key);
234
+ return r.success ? (r.data?.prompt_text_full ?? null) : null;
235
+ };
236
+ const [response, multi_doc, text, document_extract] = await Promise.all([
237
+ fetch_one('response_wrapper'),
238
+ fetch_one('multi_doc_suffix'),
239
+ fetch_one('text_wrapper'),
240
+ fetch_one('document_extract'),
241
+ ]);
242
+ const missing = [];
243
+ if (!response)
244
+ missing.push('validation/response_wrapper');
245
+ if (!multi_doc)
246
+ missing.push('validation/multi_doc_suffix');
247
+ if (!text)
248
+ missing.push('validation/text_wrapper');
249
+ if (!document_extract)
250
+ missing.push('validation/document_extract');
251
+ if (missing.length > 0) {
252
+ throw new Error(`Validation wrappers missing in hazo_prompts: ${missing.join(', ')}. Run test-app/scripts/seed-prompts.mjs.`);
253
+ }
254
+ wrapper_cache = {
255
+ response: response,
256
+ multi_doc: multi_doc,
257
+ text: text,
258
+ document_extract: document_extract,
259
+ };
260
+ return wrapper_cache;
261
+ }
174
262
  async function get_llm_api() {
175
263
  if (!llm_api_module) {
176
264
  try {
@@ -196,138 +284,122 @@ export function create_validation_route(options) {
196
284
  }
197
285
  }
198
286
  /**
199
- * Execute a single validation rule against a document.
287
+ * Call the LLM for a given input. Returns the raw response text.
288
+ * Internal: all three input kinds converge here so run_rule can stay flat.
200
289
  */
201
- async function execute_rule(rule, file_b64, mime_type, variables) {
290
+ async function call_llm_for_input(full_prompt, input, wrappers) {
202
291
  const api = await get_llm_api();
203
- // Substitute variables in the prompt
204
- const resolved_prompt = substitute_variables(rule.prompt, variables);
205
- const full_prompt = build_validation_prompt(resolved_prompt);
206
- // Call LLM
207
- let llm_response;
208
- if (is_image_mime(mime_type)) {
209
- llm_response = await api.hazo_llm_image_text({
210
- prompt: full_prompt,
211
- image_b64: file_b64,
212
- image_mime_type: mime_type,
292
+ if (input.kind === 'text') {
293
+ const text_prompt = substitute_variables(wrappers.text, {
294
+ full_prompt,
295
+ text_content: input.text_content,
213
296
  });
297
+ return api.hazo_llm_text_text({ prompt: text_prompt });
214
298
  }
215
- else if (is_document_mime(mime_type)) {
216
- llm_response = await api.hazo_llm_document_text({
299
+ if (input.kind === 'document') {
300
+ if (is_image_mime(input.mime_type)) {
301
+ return api.hazo_llm_image_text({
302
+ prompt: full_prompt,
303
+ image_b64: input.file_b64,
304
+ image_mime_type: input.mime_type,
305
+ });
306
+ }
307
+ return api.hazo_llm_document_text({
217
308
  prompt: full_prompt,
218
- document_b64: file_b64,
219
- document_mime_type: mime_type,
309
+ document_b64: input.file_b64,
310
+ document_mime_type: input.mime_type,
220
311
  });
221
312
  }
222
- else {
223
- // Fallback: treat as image
224
- llm_response = await api.hazo_llm_image_text({
313
+ // input.kind === 'documents' (batch)
314
+ const [primary, ...additional] = input.files;
315
+ const additional_documents = additional.map(f => ({
316
+ mime_type: f.mime_type,
317
+ data: f.file_b64,
318
+ }));
319
+ // If the primary is an image, fall back to extract-then-evaluate since
320
+ // image_text doesn't support additional_documents.
321
+ if (is_image_mime(primary.mime_type)) {
322
+ return batch_via_extraction(full_prompt, input.files, wrappers);
323
+ }
324
+ try {
325
+ return await api.hazo_llm_document_text({
225
326
  prompt: full_prompt,
226
- image_b64: file_b64,
227
- image_mime_type: mime_type,
327
+ document_b64: primary.file_b64,
328
+ document_mime_type: primary.mime_type,
329
+ additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
228
330
  });
229
331
  }
230
- if (!llm_response.success || !llm_response.text) {
231
- return {
232
- rule_id: rule.rule_id,
233
- has_issue: false,
234
- raw_response: llm_response.error ?? 'LLM call failed',
235
- };
332
+ catch {
333
+ // If additional_documents isn't supported, fall back.
334
+ return batch_via_extraction(full_prompt, input.files, wrappers);
236
335
  }
237
- // Parse response
238
- const parsed = parse_validation_response(llm_response.text);
239
- return {
240
- rule_id: rule.rule_id,
241
- has_issue: parsed.has_issue,
242
- issue_description: parsed.issue_description,
243
- confidence: parsed.confidence,
244
- raw_response: llm_response.text,
245
- };
246
336
  }
247
337
  /**
248
- * Execute a validation rule against multiple documents collectively.
249
- * Uses hazo_llm_document_text with additional_documents for multi-file LLM calls.
250
- * Falls back to extract-then-evaluate via hazo_llm_text_text if additional_documents is not supported.
338
+ * Fallback: extract each file individually, then evaluate collectively via
339
+ * text-only LLM. Uses validation/document_extract + validation/multi_doc_suffix.
251
340
  */
252
- async function execute_rule_batch(rule, files, variables) {
341
+ async function batch_via_extraction(full_prompt, files, wrappers) {
253
342
  const api = await get_llm_api();
254
- const resolved_prompt = substitute_variables(rule.prompt, variables);
255
- const full_prompt = build_validation_prompt(resolved_prompt);
256
- // Primary file
257
- const primary = files[0];
258
- const additional = files.slice(1);
259
- // Build additional_documents array for multi-file LLM call
260
- const additional_documents = additional.map(f => ({
261
- mime_type: f.mime_type,
262
- data: f.file_b64,
263
- }));
264
- let llm_response;
265
- try {
266
- if (is_document_mime(primary.mime_type)) {
267
- llm_response = await api.hazo_llm_document_text({
268
- prompt: full_prompt,
269
- document_b64: primary.file_b64,
270
- document_mime_type: primary.mime_type,
271
- additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
272
- });
273
- }
274
- else if (is_image_mime(primary.mime_type)) {
275
- // For images, fall back to extract-then-evaluate since image_text doesn't support additional_documents
276
- llm_response = await execute_batch_via_extraction(api, full_prompt, files);
277
- }
278
- else {
279
- llm_response = await api.hazo_llm_document_text({
280
- prompt: full_prompt,
281
- document_b64: primary.file_b64,
282
- document_mime_type: primary.mime_type,
283
- additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
284
- });
285
- }
286
- }
287
- catch {
288
- // If additional_documents not supported, fall back to extract-then-evaluate
289
- llm_response = await execute_batch_via_extraction(api, full_prompt, files);
343
+ const extractions = [];
344
+ for (const file of files) {
345
+ const resp = is_image_mime(file.mime_type)
346
+ ? await api.hazo_llm_image_text({ prompt: wrappers.document_extract, image_b64: file.file_b64, image_mime_type: file.mime_type })
347
+ : await api.hazo_llm_document_text({ prompt: wrappers.document_extract, document_b64: file.file_b64, document_mime_type: file.mime_type });
348
+ extractions.push(`FILE: ${file.file_name}\n${resp.success && resp.text ? resp.text : '[Extraction failed]'}`);
290
349
  }
350
+ const combined_prompt = substitute_variables(wrappers.multi_doc, {
351
+ full_prompt,
352
+ extractions: extractions.join('\n\n---\n\n'),
353
+ });
354
+ return api.hazo_llm_text_text({ prompt: combined_prompt });
355
+ }
356
+ /**
357
+ * Execute a single rule against a given input. This is the one place all
358
+ * three paths (single document, multi-document batch, text) converge.
359
+ */
360
+ async function run_rule(rule, input, variables, wrappers) {
361
+ const resolved_prompt = substitute_variables(rule.prompt, variables);
362
+ const full_prompt = wrap_rule_prompt(resolved_prompt, wrappers.response);
363
+ const llm_response = await call_llm_for_input(full_prompt, input, wrappers);
291
364
  if (!llm_response.success || !llm_response.text) {
292
365
  return {
293
366
  rule_id: rule.rule_id,
294
- has_issue: false,
295
- raw_response: llm_response.error ?? 'LLM batch call failed',
367
+ issues: [],
368
+ raw_response: llm_response.error ?? 'LLM call failed',
369
+ check_type: rule.check_type,
296
370
  };
297
371
  }
298
- const parsed = parse_validation_response(llm_response.text);
372
+ const parsed = parse_response(llm_response.text, rule.rule_id);
299
373
  return {
300
374
  rule_id: rule.rule_id,
301
- has_issue: parsed.has_issue,
302
- issue_description: parsed.issue_description,
303
- confidence: parsed.confidence,
375
+ issues: parsed.issues,
376
+ ...(parsed.summary ? { summary: parsed.summary } : {}),
377
+ ...(parsed.confidence !== undefined ? { confidence: parsed.confidence } : {}),
378
+ ...(parsed.extracted_data ? { extracted_data: parsed.extracted_data } : {}),
304
379
  raw_response: llm_response.text,
380
+ check_type: rule.check_type,
305
381
  };
306
382
  }
307
- /**
308
- * Fallback: extract info from each file individually, then evaluate collectively via text-only LLM.
309
- */
310
- async function execute_batch_via_extraction(api, full_prompt, files) {
311
- const extract_prompt = 'Extract all relevant information from this document: monetary amounts with descriptions, dates, payee/payer names, document type, and reference numbers. Return a concise structured summary.';
312
- const extractions = [];
313
- for (const file of files) {
314
- let resp;
315
- if (is_image_mime(file.mime_type)) {
316
- resp = await api.hazo_llm_image_text({ prompt: extract_prompt, image_b64: file.file_b64, image_mime_type: file.mime_type });
317
- }
318
- else {
319
- resp = await api.hazo_llm_document_text({ prompt: extract_prompt, document_b64: file.file_b64, document_mime_type: file.mime_type });
320
- }
321
- extractions.push(`FILE: ${file.file_name}\n${resp.success && resp.text ? resp.text : '[Extraction failed]'}`);
322
- }
323
- const combined_prompt = `${full_prompt}\n\nIMPORTANT: You are evaluating MULTIPLE documents collectively. For monetary amounts, compare the TOTAL/SUM across all documents.\n\nEXTRACTED DOCUMENT INFORMATION:\n${extractions.join('\n\n---\n\n')}`;
324
- return api.hazo_llm_text_text({ prompt: combined_prompt });
383
+ // ── File fetching helpers (route-level) ──
384
+ async function fetch_file_b64(request_url, entry) {
385
+ if (entry.file_b64)
386
+ return entry.file_b64;
387
+ if (!entry.download_url)
388
+ throw new Error(`No file_b64 or download_url for "${entry.file_name}"`);
389
+ const absolute = entry.download_url.startsWith('http')
390
+ ? entry.download_url
391
+ : `${new URL(request_url).origin}${entry.download_url}`;
392
+ const resp = await fetch(absolute);
393
+ if (!resp.ok)
394
+ throw new Error(`Failed to fetch file "${entry.file_name}": ${resp.status}`);
395
+ const buffer = Buffer.from(await resp.arrayBuffer());
396
+ return buffer.toString('base64');
325
397
  }
398
+ // ── Route ──
326
399
  return async function POST(request) {
327
400
  try {
328
401
  const body = await request.json();
329
402
  const { file_name, mime_type, download_url, file_b64: request_file_b64, rules, variables = {}, content_tag, } = body;
330
- // Validate required fields
331
403
  if (!rules?.length) {
332
404
  return Response.json({
333
405
  success: false,
@@ -336,144 +408,122 @@ export function create_validation_route(options) {
336
408
  errors: [{ rule_id: '', error: 'rules array is required and must not be empty' }],
337
409
  }, { status: 400 });
338
410
  }
339
- // Initialize LLM API
340
411
  await ensure_initialized();
341
- // ── Batch mode: validate multiple files collectively ──
412
+ const wrappers = await get_validation_wrappers();
413
+ // ── Build the ValidationInput based on request shape ──
414
+ let input;
415
+ const text_content = body.text_content;
416
+ const is_text_mode = !!text_content && !request_file_b64 && !download_url;
417
+ const rule_results = [];
418
+ const clarifications = [];
419
+ const errors = [];
342
420
  if (body.mode === 'batch' && body.files?.length) {
421
+ // Multi-file batch
343
422
  const batch_files = [];
344
- const fetch_errors = [];
345
423
  for (const f of body.files) {
346
- let b64;
347
- if (f.file_b64) {
348
- b64 = f.file_b64;
349
- }
350
- else if (f.download_url) {
351
- const absolute_url = f.download_url.startsWith('http')
352
- ? f.download_url
353
- : `${new URL(request.url).origin}${f.download_url}`;
354
- const file_response = await fetch(absolute_url);
355
- if (!file_response.ok) {
356
- fetch_errors.push({ rule_id: '', error: `Failed to fetch file "${f.file_name}": ${file_response.status}` });
357
- continue;
358
- }
359
- const buffer = Buffer.from(await file_response.arrayBuffer());
360
- b64 = buffer.toString('base64');
424
+ try {
425
+ const b64 = await fetch_file_b64(request.url, f);
426
+ batch_files.push({ file_id: f.file_id, file_name: f.file_name, mime_type: f.mime_type, file_b64: b64 });
361
427
  }
362
- else {
363
- fetch_errors.push({ rule_id: '', error: `No file_b64 or download_url for "${f.file_name}"` });
364
- continue;
428
+ catch (err) {
429
+ errors.push({ rule_id: '', error: err instanceof Error ? err.message : 'Unknown file fetch error' });
365
430
  }
366
- batch_files.push({ file_id: f.file_id, file_name: f.file_name, file_b64: b64, mime_type: f.mime_type });
367
431
  }
368
432
  if (batch_files.length === 0) {
369
433
  return Response.json({
370
- success: false, clarifications: [], rule_results: [],
371
- errors: fetch_errors.length > 0 ? fetch_errors : [{ rule_id: '', error: 'No files could be fetched for batch validation' }],
434
+ success: false,
435
+ clarifications: [],
436
+ rule_results: [],
437
+ errors: errors.length > 0 ? errors : [{ rule_id: '', error: 'No files could be fetched for batch validation' }],
372
438
  }, { status: 500 });
373
439
  }
440
+ input = { kind: 'documents', files: batch_files };
374
441
  const batch_variables = {
375
442
  ...variables,
376
443
  document_name: batch_files.map(f => f.file_name).join(', '),
377
444
  ...(content_tag ? { document_type: content_tag } : {}),
378
445
  };
379
- const batch_rule_results = [];
380
- const batch_clarifications = [];
381
- const batch_errors = [...fetch_errors];
382
446
  for (const rule of rules) {
383
447
  try {
384
- const result = await execute_rule_batch(rule, batch_files, batch_variables);
385
- batch_rule_results.push(result);
386
- if (result.has_issue && rule.clarification_type !== 'none') {
448
+ const result = await run_rule(rule, input, batch_variables, wrappers);
449
+ rule_results.push(result);
450
+ if (result.issues.length > 0 && rule.clarification_type !== 'none') {
387
451
  const clarification = create_clarification_from_result(rule, result, {
388
452
  file_name: batch_files.map(f => f.file_name).join(', '),
389
453
  mime_type: batch_files[0].mime_type,
390
454
  });
391
- // Set doc_references to include ALL files in the batch
392
455
  clarification.doc_references = batch_files.map(f => ({
393
- file_id: f.file_id,
394
- file_name: f.file_name,
395
- mime_type: f.mime_type,
456
+ file_id: f.file_id, file_name: f.file_name, mime_type: f.mime_type,
396
457
  }));
397
- batch_clarifications.push(clarification);
458
+ clarifications.push(clarification);
398
459
  }
399
460
  }
400
461
  catch (err) {
401
462
  const message = err instanceof Error ? err.message : 'Unknown error';
402
463
  on_error(err, `batch rule ${rule.rule_id}: ${rule.name}`);
403
- batch_errors.push({ rule_id: rule.rule_id, error: message });
404
- batch_rule_results.push({ rule_id: rule.rule_id, has_issue: false, raw_response: message });
464
+ errors.push({ rule_id: rule.rule_id, error: message });
465
+ rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
405
466
  }
406
467
  }
407
- return Response.json({
408
- success: batch_errors.length === 0,
409
- clarifications: batch_clarifications,
410
- rule_results: batch_rule_results,
411
- ...(batch_errors.length > 0 ? { errors: batch_errors } : {}),
412
- });
413
468
  }
414
- // ── Single file mode (default) ──
415
- // Get file as base64
416
- let file_b64;
417
- if (request_file_b64) {
418
- file_b64 = request_file_b64;
469
+ else if (is_text_mode) {
470
+ // Text mode
471
+ input = { kind: 'text', text_content: text_content, file_name };
472
+ const text_variables = {
473
+ ...variables,
474
+ document_name: file_name || 'text input',
475
+ ...(content_tag ? { document_type: content_tag } : {}),
476
+ };
477
+ for (const rule of rules) {
478
+ try {
479
+ const result = await run_rule(rule, input, text_variables, wrappers);
480
+ rule_results.push(result);
481
+ if (result.issues.length > 0 && rule.clarification_type !== 'none') {
482
+ clarifications.push(create_clarification_from_result(rule, result, { file_name: file_name || 'text input', mime_type: 'text/plain' }));
483
+ }
484
+ }
485
+ catch (err) {
486
+ const message = err instanceof Error ? err.message : 'Unknown error';
487
+ on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
488
+ errors.push({ rule_id: rule.rule_id, error: message });
489
+ rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
490
+ }
491
+ }
419
492
  }
420
- else if (download_url) {
493
+ else {
494
+ // Single document
495
+ let file_b64;
421
496
  try {
422
- const absolute_url = download_url.startsWith('http')
423
- ? download_url
424
- : `${new URL(request.url).origin}${download_url}`;
425
- const file_response = await fetch(absolute_url);
426
- if (!file_response.ok) {
427
- throw new Error(`Failed to fetch file: ${file_response.status}`);
428
- }
429
- const buffer = Buffer.from(await file_response.arrayBuffer());
430
- file_b64 = buffer.toString('base64');
497
+ file_b64 = await fetch_file_b64(request.url, { file_b64: request_file_b64, download_url, file_name });
431
498
  }
432
499
  catch (err) {
433
500
  return Response.json({
434
501
  success: false,
435
502
  clarifications: [],
436
503
  rule_results: [],
437
- errors: [{ rule_id: '', error: `Failed to fetch file "${file_name}": ${err instanceof Error ? err.message : 'Unknown error'}` }],
438
- }, { status: 500 });
504
+ errors: [{ rule_id: '', error: err instanceof Error ? err.message : 'Unknown error' }],
505
+ }, { status: err instanceof Error && err.message.includes('No file_b64') ? 400 : 500 });
439
506
  }
440
- }
441
- else {
442
- return Response.json({
443
- success: false,
444
- clarifications: [],
445
- rule_results: [],
446
- errors: [{ rule_id: '', error: 'Either file_b64 or download_url is required' }],
447
- }, { status: 400 });
448
- }
449
- // Add file context variables
450
- const all_variables = {
451
- ...variables,
452
- document_name: file_name,
453
- ...(content_tag ? { document_type: content_tag } : {}),
454
- };
455
- // Execute each rule
456
- const rule_results = [];
457
- const clarifications = [];
458
- const errors = [];
459
- for (const rule of rules) {
460
- try {
461
- const result = await execute_rule(rule, file_b64, mime_type, all_variables);
462
- rule_results.push(result);
463
- if (result.has_issue && rule.clarification_type !== 'none') {
464
- const clarification = create_clarification_from_result(rule, result, { file_name, mime_type });
465
- clarifications.push(clarification);
507
+ input = { kind: 'document', file_name, mime_type, file_b64 };
508
+ const doc_variables = {
509
+ ...variables,
510
+ document_name: file_name || 'document',
511
+ ...(content_tag ? { document_type: content_tag } : {}),
512
+ };
513
+ for (const rule of rules) {
514
+ try {
515
+ const result = await run_rule(rule, input, doc_variables, wrappers);
516
+ rule_results.push(result);
517
+ if (result.issues.length > 0 && rule.clarification_type !== 'none') {
518
+ clarifications.push(create_clarification_from_result(rule, result, { file_name, mime_type }));
519
+ }
520
+ }
521
+ catch (err) {
522
+ const message = err instanceof Error ? err.message : 'Unknown error';
523
+ on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
524
+ errors.push({ rule_id: rule.rule_id, error: message });
525
+ rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
466
526
  }
467
- }
468
- catch (err) {
469
- const message = err instanceof Error ? err.message : 'Unknown error';
470
- on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
471
- errors.push({ rule_id: rule.rule_id, error: message });
472
- rule_results.push({
473
- rule_id: rule.rule_id,
474
- has_issue: false,
475
- raw_response: message,
476
- });
477
527
  }
478
528
  }
479
529
  const response = {