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,690 @@
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
+
18
+ 检查 constraint / regex 引用的表、列是否实际存在。
19
+ 引用缺失会导致约束校验无法执行,或指向错误的数据。
20
+
21
+ 包含:
22
+ - inspect_reference_integrity:检查约束引用的表/列
23
+ - _check_table_missing / _check_column_missing:表/列缺失的 LoadingError 构建
24
+ - inspect_regex_reference_integrity:检查正则节点引用的表/列
25
+
26
+ 依赖:→ inspector_helpers(display + actions + columns)+ inspection_ids + LoadingError
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import TYPE_CHECKING
32
+
33
+ from app.shared.core.project.loader.loader_parts import inspection_ids as ids
34
+ from app.shared.core.project.loader.loader_parts.inspector_helpers import (
35
+ actions_for_node_ref,
36
+ collect_column_identifiers,
37
+ involved_entity,
38
+ is_machine_id,
39
+ regex_display,
40
+ schema_display,
41
+ )
42
+ from app.shared.core.project.loader.loader_parts.inspector_helpers import (
43
+ constraint_display as _constraint_display_fn,
44
+ )
45
+ from app.shared.core.project.loader.types import LoadingError
46
+
47
+ if TYPE_CHECKING:
48
+ from app.shared.core.project.constraint.types import ConstraintFile
49
+ from app.shared.core.project.regex.types import RegexNodeFile
50
+ from app.shared.core.project.schema.types import TableSchemaFile
51
+
52
+
53
+ def _resolve_constraint_path(constraint_id: str, constraint_paths: dict[str, str]) -> str:
54
+ """解析约束文件的相对路径,用于问题归属分组。
55
+
56
+ 优先取 manifest 引用里的显式 path;清单缺失时按 V2 命名规范
57
+ (constraints/<id>.constraint.yaml)推导,保证"按文件"分组永远有组名。
58
+ """
59
+ return constraint_paths.get(constraint_id) or f"constraints/{constraint_id}.constraint.yaml"
60
+
61
+
62
+ def _resolve_regex_path(regex_id: str, regex_paths: dict[str, str]) -> str:
63
+ """解析正则节点文件的相对路径(同 _resolve_constraint_path 的策略)。"""
64
+ return regex_paths.get(regex_id) or f"regex/{regex_id}.regex.yaml"
65
+
66
+
67
+ def _check_table_missing(
68
+ table_id: str,
69
+ constraint_id: str,
70
+ constraint_display: str,
71
+ schema_files: dict[str, TableSchemaFile],
72
+ available_schemas: list[dict],
73
+ error_prefix: str,
74
+ title_key: str,
75
+ description_key: str,
76
+ fix_hint_key: str,
77
+ loading_errors: list[LoadingError],
78
+ warnings: list[str],
79
+ role_label: str,
80
+ file_path: str = "",
81
+ ) -> None:
82
+ """检查引用的表是否存在,不存在时生成 LoadingError。
83
+
84
+ file_path 为该约束所属配置文件路径,用于前端"按文件"分组展示。
85
+ """
86
+ schema = schema_files.get(table_id)
87
+ schema_disp = schema_display(schema, fallback_id=table_id)
88
+ # 消息双留:友好显示名(人读)+ 约束 ID(定位配置文件的唯一线索,
89
+ # E2E/前端按 message 含 constraint_id 断言与解析);
90
+ # 被引用表已缺失无名称可映射,保留 ID 同理
91
+ msg = f"约束 '{constraint_display}(ID: {constraint_id})' 引用的表 '{table_id}' 不存在"
92
+ warnings.append(msg)
93
+ loading_errors.append(
94
+ LoadingError(
95
+ id=ids.ref_table_missing(error_prefix, constraint_id, table_id),
96
+ severity="blocker",
97
+ title=f"规则要用的{role_label}表找不到了",
98
+ description=(f"{constraint_display} 要用到{role_label}表「{table_id}」,但这张表可能已被删除或改名了。"),
99
+ fix_hint="点选下方一张现有的表即可。",
100
+ error_type="ReferenceIntegrityError",
101
+ file_path=file_path,
102
+ ref_id=constraint_id,
103
+ message=msg,
104
+ suggestion=f"请检查约束关联的表是否正确,可用的表: {[s.get('name') or s['id'] for s in available_schemas]}",
105
+ actions=actions_for_node_ref(constraint_id),
106
+ context={
107
+ "available_schemas": available_schemas,
108
+ "missing_table_id": table_id,
109
+ # 涉事实体:引用方约束节点(画布可按 id 定位)+ 被引用表(已缺失,无路径、不可导航)
110
+ "involved": [
111
+ involved_entity(
112
+ kind="constraint",
113
+ entity_id=constraint_id,
114
+ path=file_path,
115
+ label=constraint_display,
116
+ navigable=True,
117
+ role="referrer",
118
+ ),
119
+ involved_entity(
120
+ kind="schema",
121
+ entity_id=table_id,
122
+ label=schema_disp,
123
+ navigable=False,
124
+ role="target",
125
+ ),
126
+ ],
127
+ },
128
+ title_key=title_key,
129
+ description_key=description_key,
130
+ fix_hint_key=fix_hint_key,
131
+ message_params={
132
+ "constraintId": constraint_id,
133
+ "tableId": table_id,
134
+ "tableName": getattr(schema, "name", "") or "",
135
+ "constraintDisplay": constraint_display,
136
+ "schemaDisplay": schema_disp,
137
+ "tableIdIsMachine": is_machine_id(table_id),
138
+ },
139
+ fix_api={
140
+ "method": "POST",
141
+ "path": "/project/inspection/fix-table-ref",
142
+ "body": {"constraint_id": constraint_id, "field": error_prefix.split(":")[0], "old_table_id": table_id},
143
+ },
144
+ )
145
+ )
146
+
147
+
148
+ def _check_column_missing(
149
+ col_id: str,
150
+ table_id: str,
151
+ constraint_id: str,
152
+ constraint_display: str,
153
+ schema_files: dict[str, TableSchemaFile],
154
+ schema_column_cache: dict[str, set[str]],
155
+ error_prefix: str,
156
+ title_key: str,
157
+ description_key: str,
158
+ fix_hint_key: str,
159
+ loading_errors: list[LoadingError],
160
+ warnings: list[str],
161
+ role_label: str,
162
+ file_path: str = "",
163
+ schema_path: str = "",
164
+ ) -> None:
165
+ """检查引用的列是否存在,不存在时生成 LoadingError。
166
+
167
+ file_path 为该约束所属配置文件路径,用于前端"按文件"分组展示。
168
+ schema_path 为被引用表(此处必然存在)的配置文件路径,来自 manifest 路径映射;
169
+ 缺省时 involved 目标实体不带路径(前端仅不显示"打开文件")。
170
+ """
171
+ schema = schema_files.get(table_id)
172
+ available_cols = sorted(schema_column_cache.get(table_id, set()))
173
+ msg = f"约束 '{constraint_id}' 引用的列 '{col_id}' 在表 '{table_id}' 中不存在"
174
+ warnings.append(msg)
175
+ loading_errors.append(
176
+ LoadingError(
177
+ id=ids.ref_column_missing(error_prefix, constraint_id, table_id, col_id),
178
+ severity="blocker",
179
+ title=f"规则要用的{role_label}列找不到了",
180
+ description=(
181
+ f"{constraint_display} 要用到{role_label}表「{table_id}」的「{col_id}」列,但这一列已不存在了。"
182
+ ),
183
+ fix_hint="点选下方一个现有的列即可。",
184
+ error_type="ReferenceIntegrityError",
185
+ file_path=file_path,
186
+ ref_id=constraint_id,
187
+ message=msg,
188
+ suggestion=f"请检查列编号是否正确,表 '{table_id}' 的可用列: {available_cols}",
189
+ actions=actions_for_node_ref(constraint_id),
190
+ context={
191
+ "table_id": table_id,
192
+ "available_columns": available_cols,
193
+ "missing_column_id": col_id,
194
+ # 涉事实体:引用方约束节点(画布可按 id 定位)+ 被引用表(存在,可导航、带路径)
195
+ "involved": [
196
+ involved_entity(
197
+ kind="constraint",
198
+ entity_id=constraint_id,
199
+ path=file_path,
200
+ label=constraint_display,
201
+ navigable=True,
202
+ role="referrer",
203
+ ),
204
+ involved_entity(
205
+ kind="schema",
206
+ entity_id=table_id,
207
+ path=schema_path,
208
+ label=schema_display(schema),
209
+ navigable=True,
210
+ role="target",
211
+ ),
212
+ ],
213
+ },
214
+ title_key=title_key,
215
+ description_key=description_key,
216
+ fix_hint_key=fix_hint_key,
217
+ message_params={
218
+ "constraintId": constraint_id,
219
+ "tableId": table_id,
220
+ "tableName": getattr(schema, "name", "") or "",
221
+ "columnId": col_id,
222
+ "constraintDisplay": constraint_display,
223
+ "schemaDisplay": schema_display(schema),
224
+ "tableIdIsMachine": is_machine_id(table_id),
225
+ "columnIdIsMachine": is_machine_id(col_id),
226
+ },
227
+ fix_api={
228
+ "method": "POST",
229
+ "path": "/project/inspection/fix-column-ref",
230
+ "body": {
231
+ "constraint_id": constraint_id,
232
+ "field": error_prefix.split(":")[0],
233
+ "table_id": table_id,
234
+ "old_column_id": col_id,
235
+ },
236
+ },
237
+ )
238
+ )
239
+
240
+
241
+ def inspect_reference_integrity(
242
+ schema_files: dict[str, TableSchemaFile],
243
+ constraint_files: dict[str, ConstraintFile],
244
+ warnings: list[str],
245
+ loading_errors: list[LoadingError],
246
+ constraint_paths: dict[str, str] | None = None,
247
+ schema_paths: dict[str, str] | None = None,
248
+ ) -> None:
249
+ """检查约束引用的完整性。
250
+
251
+ constraint_paths: 约束 id → 文件相对路径(来自 manifest 引用),
252
+ 用于把问题归属到具体配置文件;缺省时按 V2 命名规范推导。
253
+ schema_paths: schema id → 文件相对路径(来自 manifest 引用),
254
+ 用于列缺失问题在 involved 清单里给"被引用表"实体带配置文件路径;缺省时留空。
255
+ """
256
+ resolved_constraint_paths = constraint_paths or {}
257
+ resolved_schema_paths = schema_paths or {}
258
+ schema_column_cache: dict[str, set[str]] = {}
259
+ for schema_id, schema_file in schema_files.items():
260
+ schema_column_cache[schema_id] = collect_column_identifiers(schema_file.columns)
261
+
262
+ available_schemas: list[dict] = [{"id": sid, "name": s.name} for sid, s in schema_files.items()]
263
+
264
+ for constraint_id, constraint_file in constraint_files.items():
265
+ refs = constraint_file.refs
266
+ if not refs:
267
+ continue
268
+
269
+ # 当前约束的归属文件路径("按文件"分组展示用)
270
+ constraint_file_path = _resolve_constraint_path(constraint_id, resolved_constraint_paths)
271
+
272
+ table_id: str | None = None
273
+ column_ids_to_check: list[str] = []
274
+
275
+ constraint_type = constraint_file.type
276
+
277
+ if constraint_type in ("NotNull", "AllowedValues", "Range", "DateLogic", "Charset", "Scripted"):
278
+ table_id = refs.get("table_id")
279
+ col = refs.get("column_id")
280
+ if col:
281
+ column_ids_to_check.append(col)
282
+
283
+ elif constraint_type == "Unique":
284
+ table_id = refs.get("table_id")
285
+ cols = refs.get("column_ids") or refs.get("column_id")
286
+ if isinstance(cols, str):
287
+ cols = [cols]
288
+ if cols:
289
+ column_ids_to_check.extend(cols)
290
+
291
+ elif constraint_type == "ForeignKey":
292
+ from_table_id = refs.get("from_table_id")
293
+ from_column_id = refs.get("from_column_id")
294
+ to_table_id = refs.get("to_table_id")
295
+ to_column_id = refs.get("to_column_id")
296
+
297
+ constraint_display = _constraint_display_fn(constraint_file)
298
+
299
+ if from_table_id:
300
+ if from_table_id not in schema_files:
301
+ _check_table_missing(
302
+ from_table_id,
303
+ constraint_id,
304
+ constraint_display,
305
+ schema_files,
306
+ available_schemas,
307
+ "fk_src_table_missing",
308
+ "inspection.issues.fk.srcTableMissing.title",
309
+ "inspection.issues.fk.srcTableMissing.description",
310
+ "inspection.issues.fk.srcTableMissing.fixHint",
311
+ loading_errors,
312
+ warnings,
313
+ "数据来源",
314
+ file_path=constraint_file_path,
315
+ )
316
+ elif from_column_id and from_column_id not in schema_column_cache.get(from_table_id, set()):
317
+ _check_column_missing(
318
+ from_column_id,
319
+ from_table_id,
320
+ constraint_id,
321
+ constraint_display,
322
+ schema_files,
323
+ schema_column_cache,
324
+ "fk_src_col_missing",
325
+ "inspection.issues.fk.srcColMissing.title",
326
+ "inspection.issues.fk.srcColMissing.description",
327
+ "inspection.issues.fk.srcColMissing.fixHint",
328
+ loading_errors,
329
+ warnings,
330
+ "数据来源",
331
+ file_path=constraint_file_path,
332
+ schema_path=resolved_schema_paths.get(from_table_id, ""),
333
+ )
334
+
335
+ if to_table_id:
336
+ if to_table_id not in schema_files:
337
+ _check_table_missing(
338
+ to_table_id,
339
+ constraint_id,
340
+ constraint_display,
341
+ schema_files,
342
+ available_schemas,
343
+ "fk_dst_table_missing",
344
+ "inspection.issues.fk.dstTableMissing.title",
345
+ "inspection.issues.fk.dstTableMissing.description",
346
+ "inspection.issues.fk.dstTableMissing.fixHint",
347
+ loading_errors,
348
+ warnings,
349
+ "关联目标",
350
+ file_path=constraint_file_path,
351
+ )
352
+ elif to_column_id and to_column_id not in schema_column_cache.get(to_table_id, set()):
353
+ _check_column_missing(
354
+ to_column_id,
355
+ to_table_id,
356
+ constraint_id,
357
+ constraint_display,
358
+ schema_files,
359
+ schema_column_cache,
360
+ "fk_dst_col_missing",
361
+ "inspection.issues.fk.dstColMissing.title",
362
+ "inspection.issues.fk.dstColMissing.description",
363
+ "inspection.issues.fk.dstColMissing.fixHint",
364
+ loading_errors,
365
+ warnings,
366
+ "关联目标",
367
+ file_path=constraint_file_path,
368
+ schema_path=resolved_schema_paths.get(to_table_id, ""),
369
+ )
370
+
371
+ continue
372
+
373
+ elif constraint_type == "Conditional":
374
+ table_id = refs.get("table_id")
375
+ then_col = refs.get("then_column_id")
376
+ if then_col:
377
+ column_ids_to_check.append(then_col)
378
+ if_conditions = refs.get("if_conditions") or []
379
+ for cond in if_conditions:
380
+ if isinstance(cond, dict):
381
+ if_col = cond.get("if_column_id")
382
+ else:
383
+ if_col = getattr(cond, "if_column_id", None)
384
+ if if_col:
385
+ column_ids_to_check.append(if_col)
386
+
387
+ elif constraint_type == "Composite":
388
+ # Composite 有两层引用需要校验:
389
+ # 1. 外层 refs.table_id —— 复合约束本身的目标表
390
+ # 2. params.sub_constraints[] —— 每个子约束有自己的 type/refs
391
+ table_id = refs.get("table_id")
392
+
393
+ # 校验外层目标表(若指定)
394
+ if table_id and table_id not in schema_files:
395
+ constraint_display = _constraint_display_fn(constraint_file)
396
+ _check_table_missing(
397
+ table_id,
398
+ constraint_id,
399
+ constraint_display,
400
+ schema_files,
401
+ available_schemas,
402
+ "composite_table_missing",
403
+ "inspection.issues.ref.tableMissing.title",
404
+ "inspection.issues.ref.tableMissing.description",
405
+ "inspection.issues.ref.tableMissing.fixHint",
406
+ loading_errors,
407
+ warnings,
408
+ "",
409
+ file_path=constraint_file_path,
410
+ )
411
+ else:
412
+ # 校验每个子约束的引用完整性(递归叶子检查)
413
+ params = getattr(constraint_file, "params", None) or {}
414
+ sub_configs = params.get("sub_constraints", []) if isinstance(params, dict) else []
415
+ for idx, sub_cfg in enumerate(sub_configs):
416
+ if not isinstance(sub_cfg, dict):
417
+ continue
418
+ sub_refs = sub_cfg.get("refs", {}) or {}
419
+ sub_table_id = sub_refs.get("table_id")
420
+ # 子约束的目标表缺失
421
+ if sub_table_id and sub_table_id not in schema_files:
422
+ constraint_display = _constraint_display_fn(constraint_file)
423
+ _check_table_missing(
424
+ sub_table_id,
425
+ constraint_id,
426
+ constraint_display,
427
+ schema_files,
428
+ available_schemas,
429
+ f"composite_sub_table_missing:{idx}",
430
+ "inspection.issues.ref.tableMissing.title",
431
+ "inspection.issues.ref.tableMissing.description",
432
+ "inspection.issues.ref.tableMissing.fixHint",
433
+ loading_errors,
434
+ warnings,
435
+ f"(子规则 #{idx + 1})",
436
+ file_path=constraint_file_path,
437
+ )
438
+ continue
439
+ # 子约束的目标列缺失
440
+ if sub_table_id and sub_table_id in schema_column_cache:
441
+ sub_type = sub_cfg.get("type", "")
442
+ sub_cols: list[str] = []
443
+ if sub_type == "Unique":
444
+ cols = sub_refs.get("column_ids") or sub_refs.get("column_id")
445
+ if isinstance(cols, str):
446
+ cols = [cols]
447
+ if cols:
448
+ sub_cols.extend(cols)
449
+ elif sub_type == "ForeignKey":
450
+ for fk_col in (sub_refs.get("from_column_id"), sub_refs.get("to_column_id")):
451
+ if fk_col:
452
+ sub_cols.append(fk_col)
453
+ else:
454
+ col = sub_refs.get("column_id")
455
+ if col:
456
+ sub_cols.append(col)
457
+ then_col = sub_refs.get("then_column_id")
458
+ if then_col:
459
+ sub_cols.append(then_col)
460
+
461
+ valid_columns = schema_column_cache[sub_table_id]
462
+ for col_id in sub_cols:
463
+ if col_id not in valid_columns:
464
+ constraint_display = _constraint_display_fn(constraint_file)
465
+ _check_column_missing(
466
+ col_id,
467
+ sub_table_id,
468
+ constraint_id,
469
+ constraint_display,
470
+ schema_files,
471
+ schema_column_cache,
472
+ f"composite_sub_col_missing:{idx}",
473
+ "inspection.issues.ref.colMissing.title",
474
+ "inspection.issues.ref.colMissing.description",
475
+ "inspection.issues.ref.colMissing.fixHint",
476
+ loading_errors,
477
+ warnings,
478
+ f"(子规则 #{idx + 1})",
479
+ file_path=constraint_file_path,
480
+ schema_path=resolved_schema_paths.get(sub_table_id, ""),
481
+ )
482
+ # Composite 已自行完成引用校验,跳过下方通用逻辑
483
+ continue
484
+
485
+ if table_id and table_id not in schema_files:
486
+ constraint_display = _constraint_display_fn(constraint_file)
487
+ _check_table_missing(
488
+ table_id,
489
+ constraint_id,
490
+ constraint_display,
491
+ schema_files,
492
+ available_schemas,
493
+ "ref_table_missing",
494
+ "inspection.issues.ref.tableMissing.title",
495
+ "inspection.issues.ref.tableMissing.description",
496
+ "inspection.issues.ref.tableMissing.fixHint",
497
+ loading_errors,
498
+ warnings,
499
+ "",
500
+ file_path=constraint_file_path,
501
+ )
502
+ continue
503
+
504
+ if table_id and table_id in schema_column_cache:
505
+ valid_columns = schema_column_cache[table_id]
506
+ for col_id in column_ids_to_check:
507
+ if col_id not in valid_columns:
508
+ constraint_display = _constraint_display_fn(constraint_file)
509
+ _check_column_missing(
510
+ col_id,
511
+ table_id,
512
+ constraint_id,
513
+ constraint_display,
514
+ schema_files,
515
+ schema_column_cache,
516
+ "ref_col_missing",
517
+ "inspection.issues.ref.colMissing.title",
518
+ "inspection.issues.ref.colMissing.description",
519
+ "inspection.issues.ref.colMissing.fixHint",
520
+ loading_errors,
521
+ warnings,
522
+ "",
523
+ file_path=constraint_file_path,
524
+ schema_path=resolved_schema_paths.get(table_id, ""),
525
+ )
526
+
527
+
528
+ def inspect_regex_reference_integrity(
529
+ regex_node_files: dict[str, RegexNodeFile],
530
+ schema_files: dict[str, TableSchemaFile],
531
+ warnings: list[str],
532
+ loading_errors: list[LoadingError],
533
+ regex_paths: dict[str, str] | None = None,
534
+ schema_paths: dict[str, str] | None = None,
535
+ ) -> None:
536
+ """检查正则节点的 source_ref 引用完整性。
537
+
538
+ regex_paths: 正则节点 id → 文件相对路径(来自 manifest 引用),
539
+ 用于把问题归属到具体配置文件;缺省时按 V2 命名规范推导。
540
+ schema_paths: schema id → 文件相对路径(来自 manifest 引用),
541
+ 用于列缺失问题在 involved 清单里给"被引用表"实体带配置文件路径;缺省时留空。
542
+ """
543
+ resolved_regex_paths = regex_paths or {}
544
+ resolved_schema_paths = schema_paths or {}
545
+ schema_column_cache: dict[str, set[str]] = {}
546
+ for table_id, schema_file in schema_files.items():
547
+ schema_column_cache[table_id] = collect_column_identifiers(schema_file.columns)
548
+
549
+ available_schemas: list[dict] = [{"id": sid, "name": s.name} for sid, s in schema_files.items()]
550
+
551
+ for regex_id, regex_file in regex_node_files.items():
552
+ source_ref = getattr(regex_file, "source_ref", None)
553
+ if not source_ref:
554
+ continue
555
+
556
+ # 当前正则节点的归属文件路径("按文件"分组展示用)
557
+ regex_file_path = _resolve_regex_path(regex_id, resolved_regex_paths)
558
+
559
+ table_id = source_ref.table_id
560
+ column_id = source_ref.column_id
561
+
562
+ if table_id and table_id not in schema_files:
563
+ regex_disp = regex_display(regex_file)
564
+ msg = f"正则节点 '{regex_id}' 引用的表 '{table_id}' 不存在"
565
+ warnings.append(msg)
566
+ loading_errors.append(
567
+ LoadingError(
568
+ id=ids.regex_table_missing(regex_id, table_id),
569
+ severity="blocker",
570
+ title="正则规则要用的表找不到了",
571
+ description=(
572
+ f"{regex_disp} 要用到 {schema_display(None, fallback_id=table_id)},"
573
+ "但这张表可能已被删除或改名了。"
574
+ ),
575
+ fix_hint="点选下方一张现有的表即可。",
576
+ error_type="ReferenceIntegrityError",
577
+ file_path=regex_file_path,
578
+ ref_id=regex_id,
579
+ message=msg,
580
+ suggestion="请检查正则节点关联的表是否正确",
581
+ actions=actions_for_node_ref(regex_id),
582
+ context={
583
+ "available_schemas": available_schemas,
584
+ "missing_table_id": table_id,
585
+ # 涉事实体:引用方正则节点(画布可按 id 定位)+ 被引用表(已缺失,无路径、不可导航)
586
+ "involved": [
587
+ involved_entity(
588
+ kind="regex",
589
+ entity_id=regex_id,
590
+ path=regex_file_path,
591
+ label=regex_disp,
592
+ navigable=True,
593
+ role="referrer",
594
+ ),
595
+ involved_entity(
596
+ kind="schema",
597
+ entity_id=table_id,
598
+ label=schema_display(None, fallback_id=table_id),
599
+ navigable=False,
600
+ role="target",
601
+ ),
602
+ ],
603
+ },
604
+ title_key="inspection.issues.regex.tableMissing.title",
605
+ description_key="inspection.issues.regex.tableMissing.description",
606
+ fix_hint_key="inspection.issues.regex.tableMissing.fixHint",
607
+ message_params={
608
+ "regexId": regex_id,
609
+ "tableId": table_id,
610
+ "tableName": "",
611
+ "regexDisplay": regex_disp,
612
+ "tableIdIsMachine": is_machine_id(table_id),
613
+ },
614
+ fix_api={
615
+ "method": "POST",
616
+ "path": "/project/inspection/fix-regex-table-ref",
617
+ "body": {"regex_id": regex_id, "old_table_id": table_id},
618
+ },
619
+ )
620
+ )
621
+ continue
622
+
623
+ if table_id and column_id and column_id not in schema_column_cache.get(table_id, set()):
624
+ available_cols = sorted(schema_column_cache.get(table_id, set()))
625
+ regex_disp = regex_display(regex_file)
626
+ msg = f"正则节点 '{regex_id}' 引用的列 '{column_id}' 在表 '{table_id}' 中不存在"
627
+ warnings.append(msg)
628
+ loading_errors.append(
629
+ LoadingError(
630
+ id=ids.regex_column_missing(regex_id, table_id, column_id),
631
+ severity="blocker",
632
+ title="正则规则要用的列找不到了",
633
+ description=(
634
+ f"{regex_disp} 要用到 {schema_display(schema_files.get(table_id))}"
635
+ f"的「{column_id}」列,但这一列已不存在了。"
636
+ ),
637
+ fix_hint="点选下方一个现有的列即可。",
638
+ error_type="ReferenceIntegrityError",
639
+ file_path=regex_file_path,
640
+ ref_id=regex_id,
641
+ message=msg,
642
+ suggestion=f"请检查列编号是否正确,表 '{table_id}' 的可用列: {available_cols}",
643
+ actions=actions_for_node_ref(regex_id),
644
+ context={
645
+ "table_id": table_id,
646
+ "available_columns": available_cols,
647
+ "missing_column_id": column_id,
648
+ # 涉事实体:引用方正则节点(画布可按 id 定位)+ 被引用表(存在,可导航、带路径)
649
+ "involved": [
650
+ involved_entity(
651
+ kind="regex",
652
+ entity_id=regex_id,
653
+ path=regex_file_path,
654
+ label=regex_disp,
655
+ navigable=True,
656
+ role="referrer",
657
+ ),
658
+ involved_entity(
659
+ kind="schema",
660
+ entity_id=table_id,
661
+ path=resolved_schema_paths.get(table_id, ""),
662
+ label=schema_display(schema_files.get(table_id)),
663
+ navigable=True,
664
+ role="target",
665
+ ),
666
+ ],
667
+ },
668
+ title_key="inspection.issues.regex.colMissing.title",
669
+ description_key="inspection.issues.regex.colMissing.description",
670
+ fix_hint_key="inspection.issues.regex.colMissing.fixHint",
671
+ message_params={
672
+ "regexId": regex_id,
673
+ "tableId": table_id,
674
+ "tableName": getattr(schema_files.get(table_id), "name", "") or "",
675
+ "columnId": column_id,
676
+ "regexDisplay": regex_disp,
677
+ "tableIdIsMachine": is_machine_id(table_id),
678
+ "columnIdIsMachine": is_machine_id(column_id),
679
+ },
680
+ fix_api={
681
+ "method": "POST",
682
+ "path": "/project/inspection/fix-regex-column-ref",
683
+ "body": {
684
+ "regex_id": regex_id,
685
+ "table_id": table_id,
686
+ "old_column_id": column_id,
687
+ },
688
+ },
689
+ )
690
+ )