precis-cli 0.1.3__py3-none-any.whl

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 (442) hide show
  1. app/__init__.py +16 -0
  2. app/api/__init__.py +32 -0
  3. app/api/dependencies.py +185 -0
  4. app/api/main.py +301 -0
  5. app/api/middleware/__init__.py +34 -0
  6. app/api/middleware/exception_handler.py +126 -0
  7. app/api/middleware/request_logging.py +97 -0
  8. app/api/middleware/token_auth.py +215 -0
  9. app/api/models/__init__.py +151 -0
  10. app/api/models/connection_rules.py +98 -0
  11. app/api/models/files.py +99 -0
  12. app/api/models/full_validation.py +554 -0
  13. app/api/models/project.py +117 -0
  14. app/api/models/projects.py +80 -0
  15. app/api/models/schema.py +121 -0
  16. app/api/models/v2_responses.py +46 -0
  17. app/api/models/validation.py +303 -0
  18. app/api/models/workspace.py +118 -0
  19. app/api/routers/__init__.py +56 -0
  20. app/api/routers/ai/__init__.py +36 -0
  21. app/api/routers/ai/chat.py +243 -0
  22. app/api/routers/ai/generate.py +156 -0
  23. app/api/routers/ai/hardware.py +199 -0
  24. app/api/routers/ai/jobs.py +544 -0
  25. app/api/routers/ai/migrate.py +471 -0
  26. app/api/routers/ai/models.py +352 -0
  27. app/api/routers/ai/ollama.py +108 -0
  28. app/api/routers/ai/providers.py +491 -0
  29. app/api/routers/ai/router.py +44 -0
  30. app/api/routers/ai/stream.py +281 -0
  31. app/api/routers/ai/utils.py +182 -0
  32. app/api/routers/core/__init__.py +34 -0
  33. app/api/routers/core/connection_rules.py +188 -0
  34. app/api/routers/core/data_sources.py +374 -0
  35. app/api/routers/core/regex.py +333 -0
  36. app/api/routers/core/reporting.py +263 -0
  37. app/api/routers/files/__init__.py +24 -0
  38. app/api/routers/files/ops.py +179 -0
  39. app/api/routers/files/transfer.py +101 -0
  40. app/api/routers/preview/__init__.py +35 -0
  41. app/api/routers/preview/content_mode.py +264 -0
  42. app/api/routers/preview/header_row.py +154 -0
  43. app/api/routers/preview/models.py +81 -0
  44. app/api/routers/preview/path_mode.py +440 -0
  45. app/api/routers/preview/router.py +30 -0
  46. app/api/routers/project/__init__.py +61 -0
  47. app/api/routers/project/base.py +111 -0
  48. app/api/routers/project/constraint.py +345 -0
  49. app/api/routers/project/full_config.py +411 -0
  50. app/api/routers/project/full_config_writer.py +327 -0
  51. app/api/routers/project/helpers.py +167 -0
  52. app/api/routers/project/inspection_fix.py +330 -0
  53. app/api/routers/project/manifest.py +605 -0
  54. app/api/routers/project/models.py +177 -0
  55. app/api/routers/project/pattern.py +268 -0
  56. app/api/routers/project/regex.py +358 -0
  57. app/api/routers/project/scanner.py +157 -0
  58. app/api/routers/project/schema.py +567 -0
  59. app/api/routers/project/schema_helpers.py +149 -0
  60. app/api/routers/project/settings.py +399 -0
  61. app/api/routers/project/template.py +325 -0
  62. app/api/routers/project/validation.py +227 -0
  63. app/api/routers/project/view.py +177 -0
  64. app/api/routers/project/workspaces.py +167 -0
  65. app/api/routers/projects/__init__.py +25 -0
  66. app/api/routers/projects/create.py +121 -0
  67. app/api/routers/projects/open.py +103 -0
  68. app/api/routers/projects/scan.py +100 -0
  69. app/api/routers/validation/__init__.py +39 -0
  70. app/api/routers/validation/common.py +263 -0
  71. app/api/routers/validation/content_mode.py +401 -0
  72. app/api/routers/validation/history.py +235 -0
  73. app/api/routers/validation/inline_mode.py +196 -0
  74. app/api/routers/validation/path_mode.py +314 -0
  75. app/api/routers/validation/router.py +51 -0
  76. app/api/services/full_validation_response_builder.py +412 -0
  77. app/api/services/io_error_messages.py +41 -0
  78. app/api/services/preview_service.py +248 -0
  79. app/cli/__init__.py +18 -0
  80. app/cli/__main__.py +30 -0
  81. app/cli/shared_services/__init__.py +42 -0
  82. app/cli/shared_services/config_ops.py +581 -0
  83. app/cli/shared_services/generation_ops.py +160 -0
  84. app/cli/shared_services/project_ops.py +288 -0
  85. app/cli/shell/__init__.py +48 -0
  86. app/cli/shell/commands/__init__.py +63 -0
  87. app/cli/shell/commands/ai/__init__.py +260 -0
  88. app/cli/shell/commands/ai/chat.py +331 -0
  89. app/cli/shell/commands/ai/delete.py +165 -0
  90. app/cli/shell/commands/ai/diff.py +70 -0
  91. app/cli/shell/commands/ai/display.py +428 -0
  92. app/cli/shell/commands/ai/executor.py +225 -0
  93. app/cli/shell/commands/ai/executor_utils.py +271 -0
  94. app/cli/shell/commands/ai/generate.py +285 -0
  95. app/cli/shell/commands/ai/interaction.py +262 -0
  96. app/cli/shell/commands/ai/migrate.py +281 -0
  97. app/cli/shell/commands/ai/resolver.py +224 -0
  98. app/cli/shell/commands/ai/status.py +101 -0
  99. app/cli/shell/commands/ai/switch.py +158 -0
  100. app/cli/shell/commands/ai/utils.py +52 -0
  101. app/cli/shell/commands/base.py +343 -0
  102. app/cli/shell/commands/config/__init__.py +131 -0
  103. app/cli/shell/commands/config/base.py +80 -0
  104. app/cli/shell/commands/config/check.py +246 -0
  105. app/cli/shell/commands/config/edit.py +148 -0
  106. app/cli/shell/commands/config/get.py +111 -0
  107. app/cli/shell/commands/config/init.py +123 -0
  108. app/cli/shell/commands/config/inspect.py +164 -0
  109. app/cli/shell/commands/config/list.py +104 -0
  110. app/cli/shell/commands/config/set.py +119 -0
  111. app/cli/shell/commands/config/show.py +150 -0
  112. app/cli/shell/commands/exit.py +67 -0
  113. app/cli/shell/commands/help.py +104 -0
  114. app/cli/shell/commands/infer_schema.py +156 -0
  115. app/cli/shell/commands/open.py +201 -0
  116. app/cli/shell/commands/project.py +215 -0
  117. app/cli/shell/commands/provider.py +619 -0
  118. app/cli/shell/commands/system.py +170 -0
  119. app/cli/shell/commands/validate.py +455 -0
  120. app/cli/shell/completer.py +231 -0
  121. app/cli/shell/config_storage.py +191 -0
  122. app/cli/shell/exceptions.py +90 -0
  123. app/cli/shell/formatter.py +371 -0
  124. app/cli/shell/interactive_menu.py +348 -0
  125. app/cli/shell/main.py +286 -0
  126. app/cli/shell/parser.py +266 -0
  127. app/cli/start.py +180 -0
  128. app/cli_main.py +52 -0
  129. app/mcp_server.py +311 -0
  130. app/shared/core/__init__.py +18 -0
  131. app/shared/core/app_version.py +59 -0
  132. app/shared/core/config/__init__.py +119 -0
  133. app/shared/core/config/server.py +193 -0
  134. app/shared/core/data_source/__init__.py +106 -0
  135. app/shared/core/data_source/loader.py +348 -0
  136. app/shared/core/data_source/loaders/__init__.py +213 -0
  137. app/shared/core/data_source/loaders/base.py +215 -0
  138. app/shared/core/data_source/loaders/converter.py +337 -0
  139. app/shared/core/data_source/loaders/csv_loader.py +252 -0
  140. app/shared/core/data_source/loaders/excel_loader.py +666 -0
  141. app/shared/core/data_source/loaders/extractor.py +268 -0
  142. app/shared/core/data_source/loaders/json_loader.py +354 -0
  143. app/shared/core/data_source/loaders/registry.py +88 -0
  144. app/shared/core/data_source/loaders/sql_loader.py +313 -0
  145. app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
  146. app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
  147. app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
  148. app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
  149. app/shared/core/data_source/schema_info.py +50 -0
  150. app/shared/core/data_source/specs/__init__.py +128 -0
  151. app/shared/core/data_source/specs/base.py +258 -0
  152. app/shared/core/data_source/specs/csv_source.py +137 -0
  153. app/shared/core/data_source/specs/excel_source.py +141 -0
  154. app/shared/core/data_source/specs/file_base.py +252 -0
  155. app/shared/core/data_source/specs/json_source.py +226 -0
  156. app/shared/core/data_source/specs/sql_source.py +161 -0
  157. app/shared/core/encoding.py +36 -0
  158. app/shared/core/io/__init__.py +24 -0
  159. app/shared/core/io/yaml.py +379 -0
  160. app/shared/core/manifest_schema/__init__.py +52 -0
  161. app/shared/core/manifest_schema/types.py +99 -0
  162. app/shared/core/manifest_schema/version.py +175 -0
  163. app/shared/core/patterns/__init__.py +24 -0
  164. app/shared/core/patterns/loader.py +134 -0
  165. app/shared/core/patterns/writer.py +244 -0
  166. app/shared/core/project/__init__.py +47 -0
  167. app/shared/core/project/constraint/builders/__init__.py +50 -0
  168. app/shared/core/project/constraint/builders/base.py +100 -0
  169. app/shared/core/project/constraint/builders/composite.py +77 -0
  170. app/shared/core/project/constraint/builders/conditional.py +67 -0
  171. app/shared/core/project/constraint/builders/foreign_key.py +53 -0
  172. app/shared/core/project/constraint/builders/registry.py +64 -0
  173. app/shared/core/project/constraint/builders/scripted.py +51 -0
  174. app/shared/core/project/constraint/builders/single_column.py +86 -0
  175. app/shared/core/project/constraint/builders/unique.py +53 -0
  176. app/shared/core/project/constraint/factory.py +170 -0
  177. app/shared/core/project/constraint/reader.py +214 -0
  178. app/shared/core/project/constraint/registry.py +233 -0
  179. app/shared/core/project/constraint/types/__init__.py +63 -0
  180. app/shared/core/project/constraint/types/constraint_file.py +261 -0
  181. app/shared/core/project/constraint/types/refs.py +460 -0
  182. app/shared/core/project/constraint/types.py +28 -0
  183. app/shared/core/project/constraint/writer.py +181 -0
  184. app/shared/core/project/loader/__init__.py +56 -0
  185. app/shared/core/project/loader/loader.py +30 -0
  186. app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
  187. app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
  188. app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
  189. app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
  190. app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
  191. app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
  192. app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
  193. app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
  194. app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
  195. app/shared/core/project/loader/loader_parts/main.py +432 -0
  196. app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
  197. app/shared/core/project/loader/loader_parts/runtime.py +125 -0
  198. app/shared/core/project/loader/types.py +246 -0
  199. app/shared/core/project/manifest/coverage.py +391 -0
  200. app/shared/core/project/manifest/reader.py +260 -0
  201. app/shared/core/project/manifest/types.py +91 -0
  202. app/shared/core/project/manifest/types_parts/__init__.py +60 -0
  203. app/shared/core/project/manifest/types_parts/constants.py +40 -0
  204. app/shared/core/project/manifest/types_parts/data_source.py +68 -0
  205. app/shared/core/project/manifest/types_parts/info.py +59 -0
  206. app/shared/core/project/manifest/types_parts/manifest.py +294 -0
  207. app/shared/core/project/manifest/types_parts/refs.py +153 -0
  208. app/shared/core/project/manifest/types_parts/settings.py +92 -0
  209. app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
  210. app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
  211. app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
  212. app/shared/core/project/manifest/types_parts/template.py +66 -0
  213. app/shared/core/project/manifest/writer.py +262 -0
  214. app/shared/core/project/manual_data/__init__.py +27 -0
  215. app/shared/core/project/manual_data/types.py +75 -0
  216. app/shared/core/project/regex/reader.py +197 -0
  217. app/shared/core/project/regex/types.py +405 -0
  218. app/shared/core/project/regex/writer.py +123 -0
  219. app/shared/core/project/schema/reader.py +170 -0
  220. app/shared/core/project/schema/types.py +47 -0
  221. app/shared/core/project/schema/types_parts/__init__.py +36 -0
  222. app/shared/core/project/schema/types_parts/column.py +174 -0
  223. app/shared/core/project/schema/types_parts/column_utils.py +72 -0
  224. app/shared/core/project/schema/types_parts/constraint.py +165 -0
  225. app/shared/core/project/schema/types_parts/schema_id.py +66 -0
  226. app/shared/core/project/schema/types_parts/source.py +255 -0
  227. app/shared/core/project/schema/types_parts/source_options.py +347 -0
  228. app/shared/core/project/schema/types_parts/table.py +230 -0
  229. app/shared/core/project/schema/writer.py +139 -0
  230. app/shared/core/project/schema_ref_check.py +95 -0
  231. app/shared/core/project/template/__init__.py +27 -0
  232. app/shared/core/project/template/expander.py +263 -0
  233. app/shared/core/project/template/reader.py +120 -0
  234. app/shared/core/project/template/types.py +114 -0
  235. app/shared/core/project/transform/reader.py +76 -0
  236. app/shared/core/project/transform/types.py +116 -0
  237. app/shared/core/project/transform/writer.py +84 -0
  238. app/shared/core/pydantic_messages.py +59 -0
  239. app/shared/core/reporter/__init__.py +46 -0
  240. app/shared/core/reporter/reporter.py +220 -0
  241. app/shared/core/reporter/reporters/__init__.py +65 -0
  242. app/shared/core/reporter/reporters/base.py +188 -0
  243. app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
  244. app/shared/core/reporter/reporters/email_reporter.py +271 -0
  245. app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
  246. app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
  247. app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
  248. app/shared/core/utils/__init__.py +18 -0
  249. app/shared/core/utils/path_utils.py +60 -0
  250. app/shared/core/utils/regex_extract.py +113 -0
  251. app/shared/domain/__init__.py +114 -0
  252. app/shared/domain/constraints/__init__.py +76 -0
  253. app/shared/domain/constraints/allowed_values.py +211 -0
  254. app/shared/domain/constraints/base.py +141 -0
  255. app/shared/domain/constraints/charset.py +337 -0
  256. app/shared/domain/constraints/composite.py +174 -0
  257. app/shared/domain/constraints/condition_registry.py +130 -0
  258. app/shared/domain/constraints/conditional.py +629 -0
  259. app/shared/domain/constraints/date_logic.py +731 -0
  260. app/shared/domain/constraints/foreign_key.py +261 -0
  261. app/shared/domain/constraints/key_normalization.py +56 -0
  262. app/shared/domain/constraints/not_null.py +185 -0
  263. app/shared/domain/constraints/range.py +360 -0
  264. app/shared/domain/constraints/regex.py +218 -0
  265. app/shared/domain/constraints/scripted.py +276 -0
  266. app/shared/domain/constraints/unique.py +233 -0
  267. app/shared/domain/data_engine.py +384 -0
  268. app/shared/domain/data_types.py +113 -0
  269. app/shared/domain/data_types_parts/__init__.py +67 -0
  270. app/shared/domain/data_types_parts/base.py +204 -0
  271. app/shared/domain/data_types_parts/composite.py +250 -0
  272. app/shared/domain/data_types_parts/expression.py +202 -0
  273. app/shared/domain/data_types_parts/extracted.py +106 -0
  274. app/shared/domain/data_types_parts/json_types.py +234 -0
  275. app/shared/domain/data_types_parts/scalars.py +719 -0
  276. app/shared/domain/data_types_parts/sequence.py +129 -0
  277. app/shared/domain/dataset_schema.py +48 -0
  278. app/shared/domain/eval_sandbox.py +63 -0
  279. app/shared/domain/expression_system.py +366 -0
  280. app/shared/domain/regex_flags.py +53 -0
  281. app/shared/domain/schema/builder.py +223 -0
  282. app/shared/domain/schema/models.py +339 -0
  283. app/shared/domain/transforms/__init__.py +28 -0
  284. app/shared/domain/transforms/aggregate.py +141 -0
  285. app/shared/domain/transforms/base.py +171 -0
  286. app/shared/domain/transforms/cast_type.py +120 -0
  287. app/shared/domain/transforms/concat.py +103 -0
  288. app/shared/domain/transforms/conditional_assign.py +114 -0
  289. app/shared/domain/transforms/date_format.py +81 -0
  290. app/shared/domain/transforms/digits.py +77 -0
  291. app/shared/domain/transforms/drop_duplicates.py +94 -0
  292. app/shared/domain/transforms/fill_na.py +94 -0
  293. app/shared/domain/transforms/filter_rows.py +97 -0
  294. app/shared/domain/transforms/lookup.py +78 -0
  295. app/shared/domain/transforms/lower_case.py +70 -0
  296. app/shared/domain/transforms/map_value.py +90 -0
  297. app/shared/domain/transforms/math_expr.py +112 -0
  298. app/shared/domain/transforms/modulo.py +82 -0
  299. app/shared/domain/transforms/regex_extract.py +103 -0
  300. app/shared/domain/transforms/registry.py +95 -0
  301. app/shared/domain/transforms/replace.py +88 -0
  302. app/shared/domain/transforms/sort_rows.py +94 -0
  303. app/shared/domain/transforms/string_split.py +84 -0
  304. app/shared/domain/transforms/strip.py +73 -0
  305. app/shared/domain/transforms/substring.py +92 -0
  306. app/shared/domain/transforms/upper_case.py +70 -0
  307. app/shared/domain/transforms/weighted_sum.py +104 -0
  308. app/shared/domain/validation_constraints.py +69 -0
  309. app/shared/services/__init__.py +62 -0
  310. app/shared/services/ai/__init__.py +48 -0
  311. app/shared/services/ai/agent/__init__.py +40 -0
  312. app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
  313. app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
  314. app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
  315. app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
  316. app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
  317. app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
  318. app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
  319. app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
  320. app/shared/services/ai/agent/executor.py +554 -0
  321. app/shared/services/ai/agent/memory.py +256 -0
  322. app/shared/services/ai/agent/planner.py +282 -0
  323. app/shared/services/ai/agent/tool_registry.py +296 -0
  324. app/shared/services/ai/agent/tools/__init__.py +32 -0
  325. app/shared/services/ai/agent/tools/config_generate.py +133 -0
  326. app/shared/services/ai/agent/tools/config_refine.py +109 -0
  327. app/shared/services/ai/agent/tools/config_validate.py +403 -0
  328. app/shared/services/ai/agent/tools/merge_results.py +136 -0
  329. app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
  330. app/shared/services/ai/agent/tools/script_parse.py +359 -0
  331. app/shared/services/ai/agent/types.py +138 -0
  332. app/shared/services/ai/chat_agent_runner.py +596 -0
  333. app/shared/services/ai/chat_orchestrator.py +725 -0
  334. app/shared/services/ai/failure_messages.py +65 -0
  335. app/shared/services/ai/job_storage.py +274 -0
  336. app/shared/services/ai/migrate_service.py +550 -0
  337. app/shared/services/ai/streaming/__init__.py +65 -0
  338. app/shared/services/ai/streaming/event_journal.py +197 -0
  339. app/shared/services/ai/streaming/orchestrator.py +262 -0
  340. app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
  341. app/shared/services/ai/streaming/sse_response.py +148 -0
  342. app/shared/services/ai/streaming/types.py +73 -0
  343. app/shared/services/ai/types.py +213 -0
  344. app/shared/services/ai/utils.py +322 -0
  345. app/shared/services/diff/config_diff.py +320 -0
  346. app/shared/services/hardware.py +237 -0
  347. app/shared/services/llm/__init__.py +62 -0
  348. app/shared/services/llm/actions/__init__.py +42 -0
  349. app/shared/services/llm/actions/_canvas_validator.py +147 -0
  350. app/shared/services/llm/actions/_constraint_validator.py +401 -0
  351. app/shared/services/llm/actions/_regex_validator.py +85 -0
  352. app/shared/services/llm/actions/_schema_validator.py +82 -0
  353. app/shared/services/llm/actions/_settings_validator.py +76 -0
  354. app/shared/services/llm/actions/_transform_validator.py +78 -0
  355. app/shared/services/llm/actions/action_handlers.py +400 -0
  356. app/shared/services/llm/actions/action_parser.py +63 -0
  357. app/shared/services/llm/actions/action_processor.py +468 -0
  358. app/shared/services/llm/actions/action_validator.py +311 -0
  359. app/shared/services/llm/actions/diff_compute.py +195 -0
  360. app/shared/services/llm/actions/regex_handlers.py +284 -0
  361. app/shared/services/llm/actions/registry.py +336 -0
  362. app/shared/services/llm/actions/schema_handlers.py +335 -0
  363. app/shared/services/llm/actions/settings_handlers.py +180 -0
  364. app/shared/services/llm/actions/specs.py +259 -0
  365. app/shared/services/llm/actions/transform_handlers.py +279 -0
  366. app/shared/services/llm/actions/validation_types.py +147 -0
  367. app/shared/services/llm/cache/__init__.py +18 -0
  368. app/shared/services/llm/cache/response_cache.py +80 -0
  369. app/shared/services/llm/chat/__init__.py +35 -0
  370. app/shared/services/llm/chat/chat_system_prompt.py +561 -0
  371. app/shared/services/llm/chat/response_parser.py +296 -0
  372. app/shared/services/llm/config/__init__.py +45 -0
  373. app/shared/services/llm/config/crypto.py +135 -0
  374. app/shared/services/llm/config/loader.py +258 -0
  375. app/shared/services/llm/config/models.py +202 -0
  376. app/shared/services/llm/config/presets.py +128 -0
  377. app/shared/services/llm/config_generator.py +163 -0
  378. app/shared/services/llm/constraints/__init__.py +41 -0
  379. app/shared/services/llm/constraints/constraint_builder.py +177 -0
  380. app/shared/services/llm/constraints/constraint_deletion.py +84 -0
  381. app/shared/services/llm/constraints/constraint_id.py +174 -0
  382. app/shared/services/llm/constraints/frontend_instructions.py +333 -0
  383. app/shared/services/llm/constraints/inline_batch.py +279 -0
  384. app/shared/services/llm/discovery/__init__.py +35 -0
  385. app/shared/services/llm/discovery/scanner.py +181 -0
  386. app/shared/services/llm/generation/__init__.py +42 -0
  387. app/shared/services/llm/generation/agent_wiring.py +156 -0
  388. app/shared/services/llm/generation/config_builder.py +386 -0
  389. app/shared/services/llm/generation/errors.py +36 -0
  390. app/shared/services/llm/generation/existing_config.py +99 -0
  391. app/shared/services/llm/generation/profiler.py +279 -0
  392. app/shared/services/llm/generation/prompt_builder.py +199 -0
  393. app/shared/services/llm/generation/response_parser.py +144 -0
  394. app/shared/services/llm/generation/service.py +622 -0
  395. app/shared/services/llm/models.py +104 -0
  396. app/shared/services/llm/providers/__init__.py +48 -0
  397. app/shared/services/llm/providers/base.py +310 -0
  398. app/shared/services/llm/providers/cached_provider.py +90 -0
  399. app/shared/services/llm/providers/ollama.py +498 -0
  400. app/shared/services/llm/providers/openai.py +334 -0
  401. app/shared/services/llm/providers/registry.py +108 -0
  402. app/shared/services/llm/schema_resolver.py +141 -0
  403. app/shared/services/llm/suggestion_utils.py +219 -0
  404. app/shared/services/llm/validate_executor.py +163 -0
  405. app/shared/services/llm/yaml_io.py +406 -0
  406. app/shared/services/preview/__init__.py +18 -0
  407. app/shared/services/preview/loader.py +145 -0
  408. app/shared/services/preview/path_validation.py +138 -0
  409. app/shared/services/project_loader.py +57 -0
  410. app/shared/services/schema_inference.py +260 -0
  411. app/shared/services/schema_runtime_builder.py +120 -0
  412. app/shared/services/validation/__init__.py +52 -0
  413. app/shared/services/validation/chunked_loader.py +509 -0
  414. app/shared/services/validation/dag/__init__.py +35 -0
  415. app/shared/services/validation/dag/builder.py +141 -0
  416. app/shared/services/validation/dag/executor.py +225 -0
  417. app/shared/services/validation/dag/sorter.py +77 -0
  418. app/shared/services/validation/data_loader.py +231 -0
  419. app/shared/services/validation/engine.py +446 -0
  420. app/shared/services/validation/executor.py +1030 -0
  421. app/shared/services/validation/extractors.py +266 -0
  422. app/shared/services/validation/history.py +209 -0
  423. app/shared/services/validation/json_payload.py +136 -0
  424. app/shared/services/validation/loader.py +152 -0
  425. app/shared/services/validation/memory_monitor.py +179 -0
  426. app/shared/services/validation/postprocess.py +208 -0
  427. app/shared/services/validation/progress.py +64 -0
  428. app/shared/services/validation/report_export.py +222 -0
  429. app/shared/services/validation/resolver.py +180 -0
  430. app/shared/services/validation/service.py +418 -0
  431. app/shared/services/validation/types.py +238 -0
  432. app/shared/services/validation/validators/__init__.py +36 -0
  433. app/shared/services/validation/validators/adapter.py +182 -0
  434. app/shared/services/validation/validators/base.py +332 -0
  435. app/shared/services/validation/validators/composite.py +233 -0
  436. app/shared/services/validation/validators/date_logic.py +276 -0
  437. app/start_server.py +133 -0
  438. precis_cli-0.1.3.dist-info/METADATA +180 -0
  439. precis_cli-0.1.3.dist-info/RECORD +442 -0
  440. precis_cli-0.1.3.dist-info/WHEEL +5 -0
  441. precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
  442. precis_cli-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,731 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 日期逻辑约束模块
18
+
19
+ 功能概述:
20
+ - 实现 DateLogicConstraint 类
21
+ - 支持日期比较和日期计算两种模式
22
+ - 用于验证日期列的逻辑正确性
23
+
24
+ 架构设计:
25
+ - 继承基类: 继承自 Constraint 抽象类
26
+ - 两种模式:
27
+ - compare: 日期比较 (大于/小于/等于某个日期)
28
+ - calculation: 日期计算 (计算年龄、天数差等)
29
+
30
+ 输入示例:
31
+ # 约束配置 - 比较模式: 出生日期必须 > 1900-01-01
32
+ constraint1 = DateLogicConstraint(
33
+ table="users",
34
+ column="birth_date",
35
+ logic_mode="compare",
36
+ compare_op="gt",
37
+ reference_date="1900-01-01"
38
+ )
39
+
40
+ # 约束配置 - 比较模式: 结束日期必须 > 开始日期
41
+ constraint2 = DateLogicConstraint(
42
+ table="projects",
43
+ column="end_date",
44
+ logic_mode="compare",
45
+ compare_op="gt",
46
+ reference_column="start_date"
47
+ )
48
+
49
+ # 约束配置 - 计算模式: 年龄必须 >= 18
50
+ constraint3 = DateLogicConstraint(
51
+ table="users",
52
+ column="birth_date",
53
+ logic_mode="calculation",
54
+ calculation_type="age",
55
+ target_value=18,
56
+ compare_op="gte"
57
+ )
58
+
59
+ 输出示例:
60
+ # 比较模式验证失败
61
+ {
62
+ "errors": [
63
+ {
64
+ "error_type": "DateLogicViolation",
65
+ "table": "users",
66
+ "row_index": 0,
67
+ "column": "birth_date",
68
+ "value": "1850-01-01",
69
+ "message": "日期逻辑约束冲突: 值 '1850-01-01' 不满足比较条件 (gt 1900-01-01)。"
70
+ }
71
+ ],
72
+ "info": {...}
73
+ }
74
+
75
+ 比较操作符:
76
+ - gt: 大于
77
+ - gte: 大于等于
78
+ - lt: 小于
79
+ - lte: 小于等于
80
+ - eq: 等于
81
+
82
+ 计算类型:
83
+ - age: 计算年龄 (当前日期 - 出生日期)
84
+ """
85
+
86
+ from __future__ import annotations
87
+
88
+ # 1. 标准库导入
89
+ from typing import Any
90
+
91
+ # 2. 第三方库导入
92
+ import pandas as pd
93
+
94
+ # 3. 项目内部导入
95
+ from app.shared.domain.constraints.base import Constraint
96
+
97
+
98
+ class DateLogicConstraint(Constraint):
99
+ """
100
+ @classdesc 日期逻辑约束
101
+
102
+ 验证日期列的逻辑正确性,支持两种模式:
103
+ 1. 比较模式: 比较日期与参考日期/参考列的大小关系
104
+ 2. 计算模式: 基于日期进行计算(如年龄),再与目标值比较
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ table: str,
110
+ column: str,
111
+ logic_mode: str = "compare",
112
+ compare_op: str | None = None,
113
+ reference_date: str | None = None,
114
+ reference_column: str | None = None,
115
+ reference_date_end: str | None = None,
116
+ reference_column_end: str | None = None,
117
+ calculation_type: str | None = None,
118
+ target_value: int | float | str | None = None,
119
+ target_column: str | None = None,
120
+ ):
121
+ """
122
+ @methoddesc 初始化日期逻辑约束
123
+
124
+ 参数:
125
+ table: 目标表名
126
+ column: 目标日期列名
127
+ logic_mode: 逻辑模式,"compare"(比较)或 "calculation"(计算)
128
+ compare_op: 比较操作符,可选 gt/gte/lt/lte/eq/range。None 时按模式取默认:
129
+ compare 模式默认 gt;calculation 模式下 age 默认 gte(满 N 岁即通过)、
130
+ days_diff 默认 eq(差值等于目标值通过)。回归 D2: 原统一默认 gt 导致 age==target
131
+ (刚满18岁)与 days_diff==target(对账周期恰好)被误判违规。
132
+ reference_date: 参考日期字符串(比较模式下使用,作为区间起点)
133
+ reference_column: 参考列名(比较模式下使用,与 reference_date 互斥,作为区间起点)
134
+ reference_date_end: 区间终点固定日期(仅 range 模式下使用)
135
+ reference_column_end: 区间终点列名(仅 range 模式下使用)
136
+ calculation_type: 计算类型(计算模式下使用),如 "age" 或 "days_diff"
137
+ target_value: 目标值(计算模式下使用),用于与计算结果比较
138
+ target_column: 目标参考列(计算模式下 days_diff 使用)
139
+ """
140
+ self.table = table
141
+ self.column = column
142
+ self.logic_mode = logic_mode
143
+ self.compare_op = compare_op
144
+ self.reference_date = reference_date
145
+ self.reference_column = reference_column
146
+ self.reference_date_end = reference_date_end
147
+ self.reference_column_end = reference_column_end
148
+ self.calculation_type = calculation_type
149
+ self.target_value = target_value
150
+ self.target_column = target_column
151
+
152
+ def _get_description(self) -> str:
153
+ """生成约束描述"""
154
+ desc = f"日期逻辑约束: {self.table}.{self.column}"
155
+ if self.logic_mode == "compare":
156
+ # 回归 D2: compare_op 为 None 时按模式回退(compare→gt)再生成描述
157
+ cmp_op = self.compare_op or "gt"
158
+ if cmp_op == "range":
159
+ start = self.reference_column or self.reference_date
160
+ end = self.reference_column_end or self.reference_date_end
161
+ desc += f" range [{start}, {end}]"
162
+ else:
163
+ ref = self.reference_column or self.reference_date
164
+ desc += f" {cmp_op} {ref}"
165
+ elif self.logic_mode == "calculation":
166
+ desc += f" {self.calculation_type} check"
167
+ return desc
168
+
169
+ def _resolve_compare_boundary(
170
+ self,
171
+ df: pd.DataFrame,
172
+ date_attr: str,
173
+ column_attr: str,
174
+ boundary_name: str = "",
175
+ ) -> tuple[Any, list[dict[str, Any]]]:
176
+ """解析比较模式下的一个边界值(固定日期或列)。
177
+
178
+ 返回:
179
+ (边界值, 错误列表)。若存在错误,边界值为 None。
180
+ """
181
+ errors: list[dict[str, Any]] = []
182
+ date_value = getattr(self, date_attr)
183
+ column_value = getattr(self, column_attr)
184
+ name_prefix = f"{boundary_name}" if boundary_name else ""
185
+
186
+ if column_value:
187
+ if column_value not in df.columns:
188
+ errors.append(
189
+ {
190
+ "error_type": "ConstraintConfigError",
191
+ "table": self.table,
192
+ "column": column_value,
193
+ "message": f"日期逻辑约束失败: {name_prefix}参考列 '{column_value}' 不在表 '{self.table}' 中。",
194
+ }
195
+ )
196
+ return None, errors
197
+ return pd.to_datetime(df[column_value], errors="coerce"), errors
198
+
199
+ if date_value:
200
+ parsed = pd.to_datetime(date_value, errors="coerce")
201
+ if pd.isna(parsed):
202
+ errors.append(
203
+ {
204
+ "error_type": "ConstraintConfigError",
205
+ "table": self.table,
206
+ "message": f"日期逻辑约束失败: 无效的{name_prefix}参考日期 '{date_value}'。",
207
+ }
208
+ )
209
+ return None, errors
210
+ return parsed, errors
211
+
212
+ return None, errors
213
+
214
+ def validate(self, datasets: dict[str, pd.DataFrame], **kwargs: Any) -> dict[str, Any]:
215
+ """
216
+ @methoddesc 执行日期逻辑验证
217
+
218
+ 参数:
219
+ datasets: 数据集字典,键为表名,值为 pandas DataFrame
220
+ **kwargs: 额外的关键字参数(本方法不使用)
221
+
222
+ 返回:
223
+ 验证结果字典,包含 errors(错误列表)和 info(约束信息)
224
+ """
225
+ errors: list[dict[str, Any]] = []
226
+
227
+ # 检查目标表是否存在
228
+ if self.table not in datasets:
229
+ errors.append(
230
+ {
231
+ "error_type": "ConstraintConfigError",
232
+ "table": self.table,
233
+ "message": f"日期逻辑约束失败: 表 '{self.table}' 不在数据集中。",
234
+ }
235
+ )
236
+ return {"errors": errors, "info": self.get_constraint_info()}
237
+
238
+ df = datasets[self.table]
239
+
240
+ # 检查目标列是否存在
241
+ if self.column not in df.columns:
242
+ errors.append(
243
+ {
244
+ "error_type": "ConstraintConfigError",
245
+ "table": self.table,
246
+ "column": self.column,
247
+ "message": f"日期逻辑约束失败: 列 '{self.column}' 不在表 '{self.table}' 中。",
248
+ }
249
+ )
250
+ return {"errors": errors, "info": self.get_constraint_info()}
251
+
252
+ # 将目标列转换为 pandas 日期时间类型,无法转换的变为 NaT(Not a Time)
253
+ target_series = pd.to_datetime(df[self.column], errors="coerce")
254
+
255
+ # §1.13: 目标列"非空但解析失败"的行报"日期无效",不再被 mask_valid 静默跳过——
256
+ # 脏值("2024-13-45"/"not_a_date")此前完全不参与该约束的任何检查。真空值豁免。
257
+ self._invalid_date_rows(df, target_series, self.column, errors)
258
+
259
+ # 模式分发:比较/计算两大分支的实现见 _validate_compare / _validate_calculation
260
+ # (extend 而非重新赋值——上面 §1.13 的无效日期条目不能被分支返回值覆盖)
261
+ if self.logic_mode == "compare":
262
+ errors.extend(self._validate_compare(df, target_series))
263
+ elif self.logic_mode == "calculation":
264
+ errors.extend(self._validate_calculation(df, target_series))
265
+ else:
266
+ # 回归: 未识别的 logic_mode 必须报配置错误。原实现 compare/calculation
267
+ # 都不匹配时静默零错误通过(fail-open),拼错的模式名让约束形同虚设。
268
+ # 守卫先例参照 charset.py 的未知 charset_mode 处理。
269
+ errors.append(
270
+ {
271
+ "error_type": "ConstraintConfigError",
272
+ "table": self.table,
273
+ "column": self.column,
274
+ "message": (
275
+ f"日期逻辑约束配置错误: 未知的 logic_mode '{self.logic_mode}',"
276
+ "支持的模式为: compare, calculation。"
277
+ ),
278
+ }
279
+ )
280
+
281
+ return {"errors": errors, "info": self.get_constraint_info()}
282
+
283
+ def _invalid_date_rows(
284
+ self,
285
+ df: pd.DataFrame,
286
+ parsed_series: pd.Series,
287
+ source_column: str | None,
288
+ errors: list[dict[str, Any]],
289
+ ) -> None:
290
+ """§1.13: 列引用侧"非空但解析失败"的行报"日期无效"(与目标列同口径,防止只修一侧)。"""
291
+ if not source_column or source_column not in df.columns:
292
+ return
293
+ raw = df[source_column]
294
+ invalid_mask = parsed_series.isna() & raw.notna() & (raw.astype(str).str.strip() != "")
295
+ for idx in df.index[invalid_mask]:
296
+ errors.append(
297
+ {
298
+ "error_type": "DateLogicError",
299
+ "table": self.table,
300
+ "row_index": int(idx),
301
+ "column": source_column,
302
+ "value": str(raw[idx]),
303
+ "message": f"日期无效: 值 '{raw[idx]}' 无法解析为日期。",
304
+ }
305
+ )
306
+
307
+ def _validate_compare(self, df: pd.DataFrame, target_series: pd.Series) -> list[dict[str, Any]]:
308
+ """
309
+ @methoddesc 比较模式校验:比较目标日期列与参考值(单边界或 range 区间)
310
+
311
+ 参数:
312
+ df: 目标表 DataFrame
313
+ target_series: 已转为 datetime 的目标列(无效值为 NaT)
314
+
315
+ 返回:
316
+ 错误记录列表(配置错误或逐行违规)
317
+ """
318
+ errors: list[dict[str, Any]] = []
319
+
320
+ # 回归 D2: compare_op 默认 None 时,compare 模式回退到 gt(保持历史行为)。
321
+ # calculation 模式的默认由各计算分支自行解析(age→gte、days_diff→eq)。
322
+ cmp_op = self.compare_op or "gt"
323
+ start_values, start_errors = self._resolve_compare_boundary(df, "reference_date", "reference_column")
324
+ if start_errors:
325
+ errors.extend(start_errors)
326
+ return errors
327
+ # §1.13: 参考列(列引用型)非空但解析失败的行报"参考日期无效"
328
+ if isinstance(start_values, pd.Series):
329
+ self._invalid_date_rows(df, start_values, self.reference_column, errors)
330
+
331
+ # range 模式需要单独解析终点边界
332
+ if cmp_op == "range":
333
+ start_is_date = bool(self.reference_date)
334
+ end_is_date = bool(self.reference_date_end)
335
+ if start_values is None or (start_is_date != end_is_date):
336
+ errors.append(
337
+ {
338
+ "error_type": "ConstraintConfigError",
339
+ "table": self.table,
340
+ "column": self.column,
341
+ "message": "日期逻辑约束失败: range 模式必须同时指定起点和终点,且两者类型一致(同为固定日期或同为列引用)。",
342
+ }
343
+ )
344
+ return errors
345
+
346
+ end_values, end_errors = self._resolve_compare_boundary(
347
+ df, "reference_date_end", "reference_column_end", "终点"
348
+ )
349
+ if end_errors:
350
+ errors.extend(end_errors)
351
+ return errors
352
+ # §1.13: range 终点参考列同样上报无效日期
353
+ if isinstance(end_values, pd.Series):
354
+ self._invalid_date_rows(df, end_values, self.reference_column_end, errors)
355
+ if end_values is None:
356
+ errors.append(
357
+ {
358
+ "error_type": "ConstraintConfigError",
359
+ "table": self.table,
360
+ "column": self.column,
361
+ "message": "日期逻辑约束失败: range 模式必须指定终点(reference_date_end 或 reference_column_end)。",
362
+ }
363
+ )
364
+ return errors
365
+
366
+ # 创建有效值掩码: 目标值、起点、终点都必须是非空的有效日期
367
+ mask_valid = target_series.notna()
368
+ if isinstance(start_values, pd.Series):
369
+ mask_valid &= start_values.notna()
370
+ if isinstance(end_values, pd.Series):
371
+ mask_valid &= end_values.notna()
372
+
373
+ # 闭区间: start <= value <= end
374
+ mask_fail = pd.Series(False, index=df.index, dtype=bool)
375
+ if not isinstance(start_values, pd.Series) and not isinstance(end_values, pd.Series):
376
+ mask_fail[mask_valid] = ~(
377
+ (target_series[mask_valid] >= start_values) & (target_series[mask_valid] <= end_values)
378
+ )
379
+ elif isinstance(start_values, pd.Series) and not isinstance(end_values, pd.Series):
380
+ mask_fail[mask_valid] = ~(
381
+ (target_series[mask_valid] >= start_values[mask_valid]) & (target_series[mask_valid] <= end_values)
382
+ )
383
+ elif not isinstance(start_values, pd.Series) and isinstance(end_values, pd.Series):
384
+ mask_fail[mask_valid] = ~(
385
+ (target_series[mask_valid] >= start_values) & (target_series[mask_valid] <= end_values[mask_valid])
386
+ )
387
+ else:
388
+ mask_fail[mask_valid] = ~(
389
+ (target_series[mask_valid] >= start_values[mask_valid])
390
+ & (target_series[mask_valid] <= end_values[mask_valid])
391
+ )
392
+
393
+ # 收集失败的行并生成错误记录
394
+ failed_indices = df.index[mask_fail]
395
+ for idx in failed_indices:
396
+ val = df.at[idx, self.column]
397
+ start_val = df.at[idx, self.reference_column] if self.reference_column else self.reference_date
398
+ end_val = (
399
+ df.at[idx, self.reference_column_end] if self.reference_column_end else self.reference_date_end
400
+ )
401
+ errors.append(
402
+ {
403
+ "error_type": "DateLogicError",
404
+ "table": self.table,
405
+ "row_index": int(idx),
406
+ "column": self.column,
407
+ "value": str(val),
408
+ "message": f"日期范围校验失败: {val} 不在 [{start_val}, {end_val}] 范围内",
409
+ }
410
+ )
411
+ else:
412
+ # 非 range 模式沿用单边界逻辑
413
+ if start_values is None:
414
+ errors.append(
415
+ {
416
+ "error_type": "ConstraintConfigError",
417
+ "table": self.table,
418
+ "message": "日期逻辑约束失败: 比较模式必须指定 reference_column 或 reference_date。",
419
+ }
420
+ )
421
+ return errors
422
+
423
+ ref_values = start_values
424
+
425
+ # 创建有效值掩码: 目标值和参考值都必须是非空的有效日期
426
+ mask_valid = target_series.notna()
427
+ if isinstance(ref_values, pd.Series):
428
+ mask_valid &= ref_values.notna()
429
+
430
+ # 初始化失败掩码(全 False)
431
+ mask_fail = pd.Series(False, index=df.index, dtype=bool)
432
+
433
+ # 根据比较操作符判断哪些行不满足条件
434
+ # ~ 表示取反,即"不满足比较条件"
435
+ if cmp_op == "gt":
436
+ mask_fail[mask_valid] = ~(
437
+ target_series[mask_valid] > ref_values
438
+ if not isinstance(ref_values, pd.Series)
439
+ else target_series[mask_valid] > ref_values[mask_valid]
440
+ )
441
+ elif cmp_op == "lt":
442
+ mask_fail[mask_valid] = ~(
443
+ target_series[mask_valid] < ref_values
444
+ if not isinstance(ref_values, pd.Series)
445
+ else target_series[mask_valid] < ref_values[mask_valid]
446
+ )
447
+ elif cmp_op == "gte":
448
+ mask_fail[mask_valid] = ~(
449
+ target_series[mask_valid] >= ref_values
450
+ if not isinstance(ref_values, pd.Series)
451
+ else target_series[mask_valid] >= ref_values[mask_valid]
452
+ )
453
+ elif cmp_op == "lte":
454
+ mask_fail[mask_valid] = ~(
455
+ target_series[mask_valid] <= ref_values
456
+ if not isinstance(ref_values, pd.Series)
457
+ else target_series[mask_valid] <= ref_values[mask_valid]
458
+ )
459
+ elif cmp_op == "eq":
460
+ mask_fail[mask_valid] = ~(
461
+ target_series[mask_valid] == ref_values
462
+ if not isinstance(ref_values, pd.Series)
463
+ else target_series[mask_valid] == ref_values[mask_valid]
464
+ )
465
+ else:
466
+ errors.append(
467
+ {
468
+ "error_type": "ConstraintConfigError",
469
+ "table": self.table,
470
+ "column": self.column,
471
+ "message": f"日期逻辑约束失败: 不支持比较操作符 '{cmp_op}',支持的操作符为 gt/gte/lt/lte/eq/range。",
472
+ }
473
+ )
474
+ return errors
475
+
476
+ # 收集失败的行并生成错误记录
477
+ failed_indices = df.index[mask_fail]
478
+ for idx in failed_indices:
479
+ val = df.at[idx, self.column]
480
+ ref_val = df.at[idx, self.reference_column] if self.reference_column else self.reference_date
481
+ errors.append(
482
+ {
483
+ "error_type": "DateLogicError",
484
+ "table": self.table,
485
+ "row_index": int(idx),
486
+ "column": self.column,
487
+ "value": str(val),
488
+ "message": f"日期比较失败: {val} 应该 {cmp_op} {ref_val}",
489
+ }
490
+ )
491
+
492
+ return errors
493
+
494
+ def _validate_calculation(self, df: pd.DataFrame, target_series: pd.Series) -> list[dict[str, Any]]:
495
+ """
496
+ @methoddesc 计算模式校验:基于日期计算(age 年龄 / days_diff 天数差)后与目标值比较
497
+
498
+ 参数:
499
+ df: 目标表 DataFrame
500
+ target_series: 已转为 datetime 的目标列(无效值为 NaT)
501
+
502
+ 返回:
503
+ 错误记录列表(配置错误或逐行违规)
504
+ """
505
+ errors: list[dict[str, Any]] = []
506
+
507
+ if self.calculation_type == "age":
508
+ # 年龄计算: 计算从出生日期到参考日期的年龄
509
+ ref_date = pd.Timestamp.now()
510
+ if self.reference_date:
511
+ ref_date = pd.to_datetime(self.reference_date, errors="coerce")
512
+
513
+ if pd.isna(ref_date):
514
+ errors.append(
515
+ {
516
+ "error_type": "ConstraintConfigError",
517
+ "table": self.table,
518
+ "message": "日期计算模式失败: 无效的参考日期。",
519
+ }
520
+ )
521
+ return errors
522
+
523
+ # 只处理非空日期
524
+ mask_valid = target_series.notna()
525
+
526
+ def calculate_age(born: pd.Timestamp) -> int | None:
527
+ """计算年龄: 年份差,再根据是否过生日调整"""
528
+ if pd.isna(born):
529
+ return None
530
+ # 年份相减,如果还没到今年的生日则再减1
531
+ return int(ref_date.year) - int(born.year) - ((ref_date.month, ref_date.day) < (born.month, born.day))
532
+
533
+ # 对有效日期计算年龄
534
+ ages = target_series[mask_valid].apply(calculate_age)
535
+
536
+ # 与目标值比较
537
+ if self.target_value is not None:
538
+ try:
539
+ target_age = float(self.target_value)
540
+ # §1.12: compare_op 归一大小写 + 未知值报配置错误(原 else 回退 gte 语义,
541
+ # ">" 这类写法静默按另一个语义执行)
542
+ op = str(self.compare_op or "gte").lower()
543
+ if op not in ("gt", "lt", "lte", "eq", "gte"):
544
+ errors.append(
545
+ {
546
+ "error_type": "ConstraintConfigError",
547
+ "table": self.table,
548
+ "column": self.column,
549
+ "message": (
550
+ f"日期计算模式配置错误: 不支持的比较操作符 '{self.compare_op}',"
551
+ "支持的操作符为 gt/gte/lt/lte/eq。"
552
+ ),
553
+ }
554
+ )
555
+ return errors
556
+ if op == "gt":
557
+ mask_fail_local = ages <= target_age
558
+ elif op == "lt":
559
+ mask_fail_local = ages >= target_age
560
+ elif op == "lte":
561
+ mask_fail_local = ages > target_age
562
+ elif op == "eq":
563
+ mask_fail_local = ages != target_age
564
+ else:
565
+ mask_fail_local = ages < target_age
566
+ op_desc = {"gt": "大于", "lt": "小于", "gte": "大于等于", "lte": "小于等于", "eq": "等于"}.get(
567
+ op, "大于等于"
568
+ )
569
+ failed_indices = mask_fail_local[mask_fail_local].index
570
+ for idx in failed_indices:
571
+ val = df.at[idx, self.column]
572
+ age = ages[idx]
573
+ errors.append(
574
+ {
575
+ "error_type": "DateLogicError",
576
+ "table": self.table,
577
+ "row_index": int(idx),
578
+ "column": self.column,
579
+ "value": str(val),
580
+ "message": f"年龄检查失败: {val} (年龄 {age}) 不满足条件 ({op_desc} {target_age})",
581
+ }
582
+ )
583
+ except ValueError as e:
584
+ errors.append(
585
+ {
586
+ "error_type": "ConstraintConfigError",
587
+ "table": self.table,
588
+ "column": self.column,
589
+ "message": f"日期计算模式的目标值「{self.target_value}」无法转换为数字({str(e)}),请检查约束配置",
590
+ }
591
+ )
592
+ else:
593
+ # 回归: 缺少 target_value 必须报配置错误。原实现静默零错误通过(fail-open),
594
+ # 用户漏配目标值时误以为年龄约束已生效。
595
+ errors.append(
596
+ {
597
+ "error_type": "ConstraintConfigError",
598
+ "table": self.table,
599
+ "column": self.column,
600
+ "message": "日期计算模式配置错误: calculation_type=age 必须指定 target_value。",
601
+ }
602
+ )
603
+
604
+ elif self.calculation_type == "days_diff":
605
+ # 天数差计算: 计算两个日期列之间的天数差
606
+ if self.target_column and self.target_column not in df.columns:
607
+ errors.append(
608
+ {
609
+ "error_type": "ConstraintConfigError",
610
+ "table": self.table,
611
+ "column": self.target_column,
612
+ "message": f"日期计算模式失败: 参考列 '{self.target_column}' 不在表 '{self.table}' 中。",
613
+ }
614
+ )
615
+ return errors
616
+
617
+ mask_valid = target_series.notna()
618
+
619
+ if self.target_column:
620
+ # 将参考列也转为日期类型
621
+ ref_series = pd.to_datetime(df[self.target_column], errors="coerce")
622
+ # §1.13: 参考列非空但解析失败的行报"日期无效"(与目标列同口径)
623
+ self._invalid_date_rows(df, ref_series, self.target_column, errors)
624
+ # 双方都必须有效
625
+ mask_valid &= ref_series.notna()
626
+
627
+ # 计算天数差(§1.19: 24h 完整天数 float,去掉 .abs() 保留符号——
628
+ # 原实现 .dt.days 截断时间成分(3d1h 算 3 天)且绝对值让 -3 满足 eq 3 双向放行)
629
+ diff_days = (target_series[mask_valid] - ref_series[mask_valid]).dt.total_seconds() / 86400
630
+
631
+ # 与目标值比较(接入 compare_op,过去硬编码 != 导致只能严格等于)
632
+ if self.target_value is not None:
633
+ try:
634
+ expected_diff = float(self.target_value)
635
+ # §1.19: 小数目标值报配置错误(按拍板原文,天数差按完整天语义只接受整数)
636
+ if expected_diff != int(expected_diff):
637
+ raise ValueError(f"天数差目标值 '{self.target_value}' 必须为非负整数天数")
638
+ # §1.12: compare_op 归一大小写 + 未知值报配置错误(原 else 回退 eq 语义)
639
+ op = str(self.compare_op or "eq").lower()
640
+ if op not in ("gt", "lt", "lte", "eq", "gte"):
641
+ errors.append(
642
+ {
643
+ "error_type": "ConstraintConfigError",
644
+ "table": self.table,
645
+ "column": self.column,
646
+ "message": (
647
+ f"日期计算模式配置错误: 不支持的比较操作符 '{self.compare_op}',"
648
+ "支持的操作符为 gt/gte/lt/lte/eq。"
649
+ ),
650
+ }
651
+ )
652
+ return errors
653
+ if op == "gt":
654
+ mask_fail_local = diff_days <= expected_diff
655
+ elif op == "lt":
656
+ mask_fail_local = diff_days >= expected_diff
657
+ elif op == "gte":
658
+ mask_fail_local = diff_days < expected_diff
659
+ elif op == "lte":
660
+ mask_fail_local = diff_days > expected_diff
661
+ else:
662
+ mask_fail_local = diff_days != expected_diff
663
+ op_desc = {
664
+ "gt": "大于",
665
+ "lt": "小于",
666
+ "gte": "大于等于",
667
+ "lte": "小于等于",
668
+ "eq": "等于",
669
+ }.get(op, "等于")
670
+ failed_indices = mask_fail_local[mask_fail_local].index
671
+ for idx in failed_indices:
672
+ val = df.at[idx, self.column]
673
+ ref_val = df.at[idx, self.target_column]
674
+ actual = diff_days[idx]
675
+ errors.append(
676
+ {
677
+ "error_type": "DateLogicError",
678
+ "table": self.table,
679
+ "row_index": int(idx),
680
+ "column": self.column,
681
+ "value": str(val),
682
+ "message": f"天数差计算结果与目标不符: {val} vs {ref_val},要求 {op_desc} {expected_diff} 天,实际 {actual} 天",
683
+ }
684
+ )
685
+ except ValueError as e:
686
+ errors.append(
687
+ {
688
+ "error_type": "ConstraintConfigError",
689
+ "table": self.table,
690
+ "column": self.column,
691
+ "message": f"target_value 转换失败: '{self.target_value}' 无法用作天数差目标值 - {str(e)}",
692
+ }
693
+ )
694
+ else:
695
+ # 回归: 缺少 target_value 必须报配置错误(与 age 分支同口径的 fail-closed 守卫)
696
+ errors.append(
697
+ {
698
+ "error_type": "ConstraintConfigError",
699
+ "table": self.table,
700
+ "column": self.column,
701
+ "message": "日期计算模式配置错误: calculation_type=days_diff 必须指定 target_value。",
702
+ }
703
+ )
704
+ else:
705
+ # 回归: 缺少 target_column 必须报配置错误。原实现整段跳过静默零错误通过(fail-open),
706
+ # 与同分支缺 target_value 的守卫(上方 else)及未知 calculation_type 守卫不对称。
707
+ errors.append(
708
+ {
709
+ "error_type": "ConstraintConfigError",
710
+ "table": self.table,
711
+ "column": self.column,
712
+ "message": "日期计算模式配置错误: calculation_type=days_diff 必须指定 target_column(参考列)。",
713
+ }
714
+ )
715
+
716
+ else:
717
+ # 回归: 未识别的 calculation_type 必须报配置错误。原实现 age/days_diff
718
+ # 都不匹配时静默零错误通过(fail-open),拼错的类型名让约束形同虚设。
719
+ errors.append(
720
+ {
721
+ "error_type": "ConstraintConfigError",
722
+ "table": self.table,
723
+ "column": self.column,
724
+ "message": (
725
+ f"日期计算模式配置错误: 未知的 calculation_type '{self.calculation_type}',"
726
+ "支持的类型为: age, days_diff。"
727
+ ),
728
+ }
729
+ )
730
+
731
+ return errors