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,165 @@
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
+ # backend/app/cli/shell/commands/ai/delete.py
17
+ """
18
+ @fileoverview AI Provider 删除命令模块
19
+
20
+ 功能概述:
21
+ - 提供 ai delete 子命令删除已配置的 AI Provider API Key
22
+ - 支持交互式菜单选择与直接指定 Provider ID 删除
23
+ - 删除前要求用户确认,防止误操作
24
+
25
+ 架构设计:
26
+ - AIDeleteCommand 继承 Command 基类
27
+ - execute() 根据是否有参数决定交互式删除或直接删除
28
+ - 通过 CLI 配置管理器删除 Provider 配置
29
+
30
+ 输入示例:
31
+ ai delete
32
+ ai delete openai
33
+
34
+ 输出示例:
35
+ CommandResult.ok("API Key 已删除")
36
+ CommandResult.error("没有已配置的 API Key")
37
+ """
38
+
39
+ from app.cli.shell.commands.base import Command, CommandResult, ProjectContext
40
+ from app.cli.shell.config_storage import get_cli_config
41
+ from app.cli.shell.formatter import Colors, Formatter
42
+ from app.cli.shell.interactive_menu import InteractiveMenu
43
+
44
+
45
+ class AIDeleteCommand(Command):
46
+ """删除已配置的 AI Provider API Key 命令。
47
+
48
+ 支持交互式选择或直接指定 Provider ID 删除。
49
+ 删除前会要求用户确认。
50
+ """
51
+
52
+ def __init__(self) -> None:
53
+ super().__init__("delete")
54
+ self._cli_config = get_cli_config()
55
+
56
+ @property
57
+ def description(self) -> str:
58
+ return "删除 AI Provider"
59
+
60
+ @property
61
+ def usage(self) -> str:
62
+ return "ai delete [provider_id]"
63
+
64
+ @property
65
+ def help_text(self) -> str:
66
+ return """
67
+ 用法: ai delete [provider_id]
68
+
69
+ 示例:
70
+ ai delete # 交互式选择要删除的 Provider
71
+ ai delete openai # 直接删除 OpenAI
72
+
73
+ 说明:
74
+ 删除指定的 Provider 配置。
75
+ 使用 'ai status' 查看已配置的 Provider。
76
+ """.strip()
77
+
78
+ def execute(self, args: list[str], context: ProjectContext) -> CommandResult:
79
+ """执行删除命令。
80
+
81
+ Args:
82
+ args: 命令参数列表,可能包含 Provider ID
83
+ context: 项目上下文
84
+
85
+ Returns:
86
+ 删除成功或失败的结果
87
+ """
88
+ providers = self._cli_config.list_providers()
89
+
90
+ if not providers:
91
+ return CommandResult.error("没有已配置的 Provider\n请使用 'ai setup' 进行配置")
92
+
93
+ if not args:
94
+ return self._interactive_delete(providers)
95
+
96
+ # 4.4: 大小写不敏感匹配——原实现把参数强转小写后查存储原名(含大写的
97
+ # provider 恒查不到),交互菜单走原值正常,参数路径自相矛盾
98
+ raw_id = args[0]
99
+ provider_id = next(
100
+ (p.id for p in providers if p.id.lower() == raw_id.lower()),
101
+ raw_id,
102
+ )
103
+ return self._do_delete(provider_id, providers)
104
+
105
+ def _interactive_delete(self, providers: list) -> CommandResult:
106
+ """交互式删除 Provider(支持方向键导航)。
107
+
108
+ 显示菜单让用户选择要删除的 Provider。
109
+
110
+ Args:
111
+ providers: 已配置的 Provider 列表
112
+
113
+ Returns:
114
+ 删除结果或取消结果
115
+ """
116
+ print(Formatter.header("\n删除 Provider"))
117
+
118
+ menu = InteractiveMenu("请选择要删除的 Provider:")
119
+
120
+ for p in providers:
121
+ label = f"{p.name} ({p.model})"
122
+ menu.add_item(p.id, label, "")
123
+
124
+ menu.add_item("_cancel_", "取消", "返回")
125
+
126
+ provider_id = menu.show()
127
+
128
+ if provider_id is None or provider_id == "_cancel_":
129
+ return CommandResult.ok("已取消")
130
+
131
+ return self._do_delete(provider_id, providers)
132
+
133
+ def _do_delete(self, provider_id: str, providers: list) -> CommandResult:
134
+ """执行删除操作。
135
+
136
+ 验证 Provider 存在,然后要求用户确认后删除。
137
+
138
+ Args:
139
+ provider_id: 要删除的 Provider ID
140
+ providers: 已配置的 Provider 列表
141
+
142
+ Returns:
143
+ 删除成功或失败的结果
144
+ """
145
+ provider = self._cli_config.get_provider(provider_id)
146
+ if not provider:
147
+ return CommandResult.error(f"Provider '{provider_id}' 未配置\n使用 'ai status' 查看已配置的 Provider")
148
+
149
+ print(Formatter.warning(f"\n警告: 将删除 {provider.name} ({provider.id})"))
150
+ print(Formatter.info(f" 模型: {provider.model}\n"))
151
+
152
+ try:
153
+ confirm = input(Formatter.colorize("确认删除? (y/N): ", Colors.YELLOW)).strip().lower()
154
+ except (KeyboardInterrupt, EOFError):
155
+ print()
156
+ return CommandResult.ok("已取消")
157
+
158
+ if confirm not in ("y", "yes"):
159
+ return CommandResult.ok("已取消")
160
+
161
+ if self._cli_config.delete_provider(provider_id):
162
+ print(Formatter.success(f"\n[*] {provider.name} 已删除"))
163
+ return CommandResult.ok("Provider 已删除")
164
+ else:
165
+ return CommandResult.error("删除失败")
@@ -0,0 +1,70 @@
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
+ # backend/app/cli/shell/commands/ai/diff.py
17
+ """
18
+ @fileoverview AI 配置变更 Diff 生成模块
19
+
20
+ 功能概述:
21
+ - 使用 Python 标准库 difflib 生成统一差异格式(Unified Diff)
22
+ - 对比文件修改前后的内容,输出类 Git diff 格式
23
+ - 处理行尾换行符,确保 diff 格式标准
24
+
25
+ 架构设计:
26
+ - 独立工具模块,无外部依赖(仅使用标准库 difflib)
27
+ - 被 display.py 调用以展示详细的文件变更对比
28
+ - 统一差异格式(unified diff)是 Git 等版本控制工具使用的标准格式
29
+
30
+ 输入示例:
31
+ _generate_diff("constraints.yaml", old_content, new_content)
32
+
33
+ 输出示例:
34
+ "--- a/constraints.yaml\n+++ b/constraints.yaml\n@@ -1,3 +1,4 @@..."
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import difflib
40
+ from pathlib import Path
41
+
42
+
43
+ def _generate_diff(file_path: str, old_content: str, new_content: str) -> str:
44
+ """生成文件的 diff 显示。
45
+
46
+ 使用 Python 标准库 difflib.unified_diff 生成统一差异格式(Unified Diff),
47
+ 这是 Git 等版本控制工具使用的标准格式。
48
+
49
+ Args:
50
+ file_path: 文件路径(用于显示文件名)
51
+ old_content: 修改前的文件内容
52
+ new_content: 修改后的文件内容
53
+
54
+ Returns:
55
+ 统一差异格式的字符串
56
+ """
57
+ from_line = old_content.splitlines(keepends=True) if old_content else []
58
+ to_line = new_content.splitlines(keepends=True) if new_content else []
59
+
60
+ # 确保最后一行以换行符结尾,否则 diff 格式不标准
61
+ if from_line and not from_line[-1].endswith("\n"):
62
+ from_line[-1] += "\n"
63
+ if to_line and not to_line[-1].endswith("\n"):
64
+ to_line[-1] += "\n"
65
+
66
+ diff = difflib.unified_diff(
67
+ from_line, to_line, fromfile=f"a/{Path(file_path).name}", tofile=f"b/{Path(file_path).name}", lineterm="\n"
68
+ )
69
+
70
+ return "".join(diff)
@@ -0,0 +1,428 @@
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
+ # backend/app/cli/shell/commands/ai/display.py
17
+ """
18
+ @fileoverview AI 执行结果展示模块
19
+
20
+ 功能概述:
21
+ - 显示 AI 修改文件的 diff 摘要与详细对比
22
+ - 区分新增、删除、修改三种文件状态并用颜色高亮
23
+ - 展示约束操作结果与数据校验结果(通过/失败)
24
+
25
+ 架构设计:
26
+ - _show_diff_summary(): 显示文件变更摘要,询问是否查看详细 diff
27
+ - _display_detailed_diff(): 使用颜色高亮显示详细 diff 内容
28
+ - _display_execution_results(): 主入口,判断是否有文件修改并决定展示方式
29
+ - _display_results_without_diff(): 无文件修改时的结果展示(如仅执行校验)
30
+ - _display_constraint_results(): 分类显示校验结果与约束操作结果
31
+
32
+ 输入示例:
33
+ _display_execution_results(result, "/path/to/project", original_files_cache)
34
+
35
+ 输出示例:
36
+ 带颜色高亮的 diff 与操作结果文本
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import logging
42
+ from pathlib import Path
43
+
44
+ from app.cli.shell.formatter import Colors, Formatter
45
+ from app.shared.services.ai.chat_orchestrator import ChatExecutionResult
46
+ from app.shared.services.llm.actions.validation_types import action_display_name
47
+
48
+ from .diff import _generate_diff
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ # 工具名到中文标签的映射(与 ChatAgentRunner._TOOL_LABELS 保持一致)
54
+ _TOOL_LABELS = {
55
+ "read_project": "读取项目",
56
+ "read_table": "查看数据",
57
+ "apply_actions": "修改配置",
58
+ "validate_table": "校验数据",
59
+ "read_canvas": "读取画布",
60
+ }
61
+
62
+
63
+ def _display_tool_trail(tool_steps: list[dict]) -> None:
64
+ """显示 Agent 工具调用轨迹。
65
+
66
+ 在 CLI 交互模式下,当 AI 使用 Agent 工具循环完成查-改-验后,
67
+ 输出简要的工具步骤摘要,让用户感知 AI 的思考与操作路径。
68
+
69
+ Args:
70
+ tool_steps: ChatAgentRunResult.tool_steps 列表,每个元素含 tool/label/turn/action_count/status
71
+ """
72
+ if not tool_steps:
73
+ return
74
+
75
+ print(Formatter.header("\n🔧 Agent 工具轨迹"))
76
+ for step in tool_steps:
77
+ label = step.get("label") or _TOOL_LABELS.get(step.get("tool", ""), step.get("tool", "未知"))
78
+ action_count = step.get("action_count")
79
+ status = step.get("status", "success")
80
+ error = step.get("error")
81
+
82
+ count_str = f"({action_count})" if action_count else ""
83
+ status_marker = "✓" if status == "success" else "✗" if status == "failed" else "•"
84
+ line = f" {status_marker} {label}{count_str}"
85
+ if error:
86
+ line += f" — {error}"
87
+ print(Formatter.info(line))
88
+
89
+
90
+ def _show_diff_summary(changed_files: dict[str, tuple[str, str]]) -> bool:
91
+ """显示修改文件的 diff 摘要,并询问用户是否查看详细 diff。
92
+
93
+ 对每份修改的文件显示状态:新增(绿色)、删除(红色)、修改(黄色)。
94
+
95
+ Args:
96
+ changed_files: 字典,键为文件路径,值为 (旧内容, 新内容) 元组
97
+
98
+ Returns:
99
+ 用户是否选择查看详细 diff
100
+ """
101
+ if not changed_files:
102
+ return False
103
+
104
+ print(Formatter.header("\n📋 修改文件摘要"))
105
+ print("=" * 50)
106
+
107
+ for file_path, (old_content, new_content) in changed_files.items():
108
+ rel_path = Path(file_path).name
109
+ if old_content == "":
110
+ status = "新增"
111
+ color = Colors.GREEN
112
+ elif new_content == "":
113
+ status = "删除"
114
+ color = Colors.RED
115
+ else:
116
+ old_lines = len(old_content.splitlines())
117
+ new_lines = len(new_content.splitlines())
118
+ if old_lines == new_lines:
119
+ status = "修改"
120
+ else:
121
+ status = f"修改 ({old_lines} → {new_lines} 行)"
122
+ color = Colors.YELLOW
123
+
124
+ print(f" {Formatter.colorize(status, color)}: {rel_path}")
125
+
126
+ print("=" * 50)
127
+
128
+ try:
129
+ response = input(Formatter.colorize("\n查看详细 diff? (y/N): ", Colors.YELLOW)).strip().lower()
130
+ return response in ("y", "yes")
131
+ except (KeyboardInterrupt, EOFError):
132
+ print()
133
+ return False
134
+
135
+
136
+ def _display_detailed_diff(changed_files: dict[str, tuple[str, str]]) -> None:
137
+ """显示详细的 diff 内容。
138
+
139
+ 对 diff 的每一行进行颜色高亮:
140
+ - 绿色:新增的行(以 + 开头)
141
+ - 红色:删除的行(以 - 开头)
142
+ - 青色:行号信息(以 @@ 开头)
143
+ - 灰色:文件头信息(以 --- 或 +++ 开头)
144
+
145
+ Args:
146
+ changed_files: 字典,键为文件路径,值为 (旧内容, 新内容) 元组
147
+ """
148
+ print(Formatter.header("\n📄 详细 Diff"))
149
+ print("=" * 70)
150
+
151
+ has_diff = False
152
+ for file_path, (old_content, new_content) in changed_files.items():
153
+ diff_text = _generate_diff(file_path, old_content, new_content)
154
+ if diff_text:
155
+ has_diff = True
156
+ rel_path = Path(file_path).name
157
+ print(f"\n{Formatter.colorize('▶', Colors.CYAN)} {rel_path}")
158
+ print("-" * 50)
159
+
160
+ for line in diff_text.splitlines():
161
+ if line.startswith("+"):
162
+ print(Formatter.colorize(line, Colors.GREEN))
163
+ elif line.startswith("-"):
164
+ print(Formatter.colorize(line, Colors.RED))
165
+ elif line.startswith("@@"):
166
+ print(Formatter.colorize(line, Colors.CYAN))
167
+ elif line.startswith("---") or line.startswith("+++"):
168
+ print(Formatter.dim(line))
169
+ else:
170
+ print(line)
171
+
172
+ if not has_diff:
173
+ print(Formatter.dim(" 无内容变更"))
174
+
175
+ print("\n" + "=" * 70)
176
+
177
+
178
+ def _display_execution_results(
179
+ result: ChatExecutionResult,
180
+ project_path: str,
181
+ original_files_cache: dict[str, str] | None = None,
182
+ ) -> None:
183
+ """显示执行结果(CLI 交互模式)并处理 diff。
184
+
185
+ 判断 AI 返回的动作是否包含约束节点的增删改操作。
186
+ 如果有文件修改,则对比原始缓存生成 diff 并展示;
187
+ 如果没有文件修改(如仅执行校验),则只显示校验结果。
188
+
189
+ Args:
190
+ result: AIChatOrchestrator 的执行结果对象
191
+ project_path: 项目路径
192
+ original_files_cache: 原始文件内容缓存,用于 diff 对比
193
+ """
194
+ actions = result.actions
195
+ original_files_cache = original_files_cache or {}
196
+
197
+ # 判断是否有会修改文件的操作
198
+ file_modification_types = {
199
+ "ADD_CONSTRAINT_NODE",
200
+ "UPDATE_CONSTRAINT_NODE",
201
+ "DELETE_CONSTRAINT_NODE",
202
+ "ADD_SCHEMA",
203
+ "UPDATE_SCHEMA",
204
+ "DELETE_SCHEMA",
205
+ "ADD_REGEX",
206
+ "UPDATE_REGEX",
207
+ "DELETE_REGEX",
208
+ "ADD_TRANSFORM",
209
+ "UPDATE_TRANSFORM",
210
+ "DELETE_TRANSFORM",
211
+ "UPDATE_SETTINGS",
212
+ }
213
+ has_modifications = any(a.get("actionType") in file_modification_types for a in actions)
214
+
215
+ if not has_modifications:
216
+ _display_results_without_diff(result)
217
+ return
218
+
219
+ # 使用 orchestrator 已经执行的结果,避免重复执行 process_actions
220
+ results = getattr(result, "action_results", [])
221
+
222
+ # 收集修改后的文件内容,与原始缓存对比找出变更
223
+ changed_files = {}
224
+ for file_path, old_content in original_files_cache.items():
225
+ try:
226
+ if Path(file_path).exists():
227
+ new_content = Path(file_path).read_text(encoding="utf-8")
228
+ else:
229
+ new_content = ""
230
+ if old_content != new_content:
231
+ changed_files[file_path] = (old_content, new_content)
232
+ except Exception:
233
+ logger.error("读取文件内容失败", exc_info=True)
234
+
235
+ _display_constraint_results(results)
236
+
237
+ if changed_files and _show_diff_summary(changed_files):
238
+ _display_detailed_diff(changed_files)
239
+
240
+
241
+ def _display_results_without_diff(result: ChatExecutionResult) -> None:
242
+ """显示没有修改操作的结果(仅校验等)。
243
+
244
+ 当 AI 的动作不涉及文件修改时(如 VALIDATE_PROJECT),
245
+ 直接显示校验结果而不进行 diff 对比。
246
+
247
+ Args:
248
+ result: AIChatOrchestrator 的执行结果对象
249
+ """
250
+ results = getattr(result, "action_results", [])
251
+ _display_constraint_results(results)
252
+
253
+
254
+ def _format_validate_table_display(table_filter: str | list[str] | None) -> str:
255
+ """根据校验的 table_filter 推导出用于展示的"表"描述文案。
256
+
257
+ 校验结果详情(validate_details)里的 table_filter 才是真实信息源:
258
+ - None/空 → 全量校验
259
+ - 字符串 → 单表校验
260
+ - 列表 → 多表校验
261
+
262
+ Args:
263
+ table_filter: validate_executor 返回的 details.table_filter 值
264
+
265
+ Returns:
266
+ 适合直接拼接进 "✓ 表 '...' 数据校验通过" 的描述文案
267
+ """
268
+ # 全量校验(None 或空字符串/空列表)
269
+ if not table_filter:
270
+ return "全部表"
271
+ # 单表(字符串)
272
+ if isinstance(table_filter, str):
273
+ return table_filter
274
+ # 列表形式
275
+ tables = [t for t in table_filter if t]
276
+ if not tables:
277
+ return "全部表"
278
+ if len(tables) == 1:
279
+ return tables[0]
280
+ return f"{len(tables)} 张表({', '.join(tables)})"
281
+
282
+
283
+ def _display_constraint_results(results: list[dict]) -> None:
284
+ """显示操作结果。
285
+
286
+ 将动作结果分为四类:
287
+ 1. VALIDATE_PROJECT:数据校验结果
288
+ 2. 约束操作:约束 CRUD 结果
289
+ 3. Schema/Regex/Transform/Settings 操作
290
+ 4. 其他操作
291
+
292
+ Args:
293
+ results: 动作执行结果列表,每个元素是一个字典
294
+ """
295
+ constraint_types = {"ADD_CONSTRAINT_NODE", "UPDATE_CONSTRAINT_NODE", "DELETE_CONSTRAINT_NODE"}
296
+ schema_types = {"ADD_SCHEMA", "UPDATE_SCHEMA", "DELETE_SCHEMA"}
297
+ regex_types = {"ADD_REGEX", "UPDATE_REGEX", "DELETE_REGEX"}
298
+ transform_types = {"ADD_TRANSFORM", "UPDATE_TRANSFORM", "DELETE_TRANSFORM"}
299
+
300
+ # 分离各类结果
301
+ validate_results = [r for r in results if r.get("action", {}).get("actionType") == "VALIDATE_PROJECT"]
302
+ constraint_results = [r for r in results if r.get("action", {}).get("actionType") in constraint_types]
303
+ schema_results = [r for r in results if r.get("action", {}).get("actionType") in schema_types]
304
+ regex_results = [r for r in results if r.get("action", {}).get("actionType") in regex_types]
305
+ transform_results = [r for r in results if r.get("action", {}).get("actionType") in transform_types]
306
+ settings_results = [r for r in results if r.get("action", {}).get("actionType") == "UPDATE_SETTINGS"]
307
+
308
+ # 显示校验结果
309
+ for r in validate_results:
310
+ msg = r.get("message", "")
311
+ details = r.get("validate_details", {})
312
+ has_errors = details.get("has_errors", False) if details else False
313
+ # 展示文案优先取 action.constraintSpec 里的"原始表名"(用户友好):
314
+ # - 多表:spec.tables(表名列表)
315
+ # - 单表:spec.tableName(表名,resolver 解析后仍保留原名)
316
+ # - 全量:两者都没有 → fallback 到 validate_details.table_filter(None)
317
+ # 注意:validate_details.table_filter 在 resolver 解析后会变成"表 ID"(加密串),
318
+ # 直接展示对用户不友好,故仅作兜底。
319
+ spec = r.get("action", {}).get("constraintSpec", {}) or {}
320
+ raw_tables = spec.get("tables")
321
+ if isinstance(raw_tables, list) and raw_tables:
322
+ table_display = _format_validate_table_display(raw_tables)
323
+ else:
324
+ single_name = spec.get("tableName")
325
+ fallback = single_name or (details.get("table_filter") if details else None)
326
+ table_display = _format_validate_table_display(fallback)
327
+
328
+ if has_errors:
329
+ print(Formatter.warning("\n" + "=" * 50))
330
+ print(Formatter.warning(f"⚠ 表 '{table_display}' 数据校验发现问题"))
331
+ print(Formatter.warning("=" * 50))
332
+ print(msg)
333
+ print("=" * 50)
334
+ else:
335
+ print(Formatter.success("\n" + "=" * 50))
336
+ print(Formatter.success(f"✓ 表 '{table_display}' 数据校验通过"))
337
+ print(Formatter.success(f" 耗时: {details.get('duration_ms', 0)}ms"))
338
+ print(Formatter.success("=" * 50))
339
+
340
+ # 显示约束操作结果
341
+ if constraint_results:
342
+ if all(r.get("success") for r in constraint_results):
343
+ print(Formatter.success("\n" + "=" * 50))
344
+ print(Formatter.success("✓ 约束操作成功"))
345
+ print(Formatter.success("=" * 50))
346
+
347
+ for r in constraint_results:
348
+ msg = r.get("message", "N/A")
349
+ action = r.get("action", {})
350
+ spec = action.get("constraintSpec", {})
351
+ is_inline = spec.get("isInline", False)
352
+ constraint_type = spec.get("type", "N/A")
353
+ table_name = spec.get("tableName", "未知表")
354
+ column_name = spec.get("targetColumn", "N/A")
355
+ params = spec.get("params", {})
356
+
357
+ print("\n 约束配置详情:")
358
+ print(f" - 类型: {constraint_type}")
359
+ print(f" - 目标表: {table_name}")
360
+ print(f" - 目标列: {column_name}")
361
+
362
+ if params:
363
+ print(f" - 参数: {params}")
364
+
365
+ if is_inline:
366
+ print(" - 存储方式: 内联约束(存储在表配置中)")
367
+ else:
368
+ constraint_id = msg.replace("inline:", "") if msg.startswith("inline:") else msg
369
+ print(" - 存储方式: 独立约束文件")
370
+ print(f" - 文件路径: constraints/{constraint_id}.constraint.yaml")
371
+
372
+ print("\n" + "=" * 50)
373
+ else:
374
+ print(Formatter.error("\n" + "=" * 50))
375
+ print(Formatter.error("✗ 约束操作失败"))
376
+ print(Formatter.error("=" * 50))
377
+ for r in constraint_results:
378
+ if not r.get("success"):
379
+ msg = r.get("message", "未知错误")
380
+ print(f" - 错误: {msg}")
381
+ print("=" * 50)
382
+
383
+ # 显示 Schema 操作结果
384
+ if schema_results:
385
+ print(Formatter.header("\n📋 Schema 操作"))
386
+ for r in schema_results:
387
+ action_type = r.get("action", {}).get("actionType", "")
388
+ spec = r.get("action", {}).get("schemaSpec", {})
389
+ name = spec.get("name", spec.get("schemaId", "未知"))
390
+ if r.get("success"):
391
+ print(Formatter.success(f" ✓ {action_display_name(action_type)}: {name}"))
392
+ else:
393
+ print(Formatter.error(f" ✗ {action_display_name(action_type)}: {name} - {r.get('message', '')}"))
394
+
395
+ # 显示 Regex 操作结果
396
+ if regex_results:
397
+ print(Formatter.header("\n🔤 Regex 操作"))
398
+ for r in regex_results:
399
+ action_type = r.get("action", {}).get("actionType", "")
400
+ spec = r.get("action", {}).get("regexSpec", {})
401
+ name = spec.get("name", spec.get("regexId", "未知"))
402
+ if r.get("success"):
403
+ print(Formatter.success(f" ✓ {action_display_name(action_type)}: {name}"))
404
+ else:
405
+ print(Formatter.error(f" ✗ {action_display_name(action_type)}: {name} - {r.get('message', '')}"))
406
+
407
+ # 显示 Transform 操作结果
408
+ if transform_results:
409
+ print(Formatter.header("\n⚙ Transform 操作"))
410
+ for r in transform_results:
411
+ action_type = r.get("action", {}).get("actionType", "")
412
+ spec = r.get("action", {}).get("transformSpec", {})
413
+ t_type = spec.get("type", spec.get("transformId", "未知"))
414
+ if r.get("success"):
415
+ print(Formatter.success(f" ✓ {action_display_name(action_type)}: {t_type}"))
416
+ else:
417
+ print(Formatter.error(f" ✗ {action_display_name(action_type)}: {t_type} - {r.get('message', '')}"))
418
+
419
+ # 显示 Settings 操作结果
420
+ if settings_results:
421
+ print(Formatter.header("\n⚙ 项目设置"))
422
+ for r in settings_results:
423
+ spec = r.get("action", {}).get("settingsSpec", {})
424
+ category = spec.get("category", "未知")
425
+ if r.get("success"):
426
+ print(Formatter.success(f" ✓ 更新设置: {category}"))
427
+ else:
428
+ print(Formatter.error(f" ✗ 更新设置失败: {r.get('message', '')}"))