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,225 @@
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
+ """@fileoverview AI 聊天命令执行器
17
+
18
+ 功能概述:
19
+ - 执行 AI 聊天命令的主入口
20
+ - 辅助函数已拆分到 executor_utils.py / diff.py / display.py
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+
27
+ from app.cli.shell.commands.base import CommandResult, ProjectContext
28
+ from app.cli.shell.formatter import Formatter
29
+ from app.shared.services.ai.chat_orchestrator import AIChatOrchestrator, ChatOptions
30
+ from app.shared.services.llm.providers.base import compute_token_budgets, resolve_context_window
31
+
32
+ from .display import _display_execution_results, _display_tool_trail
33
+ from .executor_utils import (
34
+ SpinnerController,
35
+ _collect_all_config_files,
36
+ _collect_declared_new_files,
37
+ _get_provider_display,
38
+ _get_provider_with_key,
39
+ )
40
+ from .interaction import confirm_actions as base_confirm_actions
41
+ from .resolver import resolve_ambiguities as base_resolve_ambiguities
42
+
43
+ # 为模型回复预留的 token 预算
44
+ RESERVED_OUTPUT_TOKENS = 8000
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ def execute_ai_chat(
50
+ message: str,
51
+ context: ProjectContext,
52
+ interactive: bool = False,
53
+ history: list[dict[str, str]] | None = None,
54
+ agent_mode: bool = True,
55
+ ) -> CommandResult:
56
+ """执行 AI 对话(统一版本)。
57
+
58
+ 这是 CLI 中执行 AI 对话的核心入口函数,负责:
59
+ 1. 检查项目是否已打开
60
+ 2. 获取并验证 Provider 配置
61
+ 3. 构建 AIChatOrchestrator 和 ChatOptions
62
+ 4. 在后台运行异步对话任务
63
+ 5. 显示结果和 diff
64
+
65
+ Args:
66
+ message: 用户输入的消息内容
67
+ context: 命令上下文,必须包含打开的项目路径
68
+ interactive: 是否为交互模式(显示 spinner、提示确认等)
69
+ history: 对话历史记录列表,每个元素包含 role 和 content
70
+ agent_mode: 是否启用 Agent 深度模式(默认 True),CLI 无画布时仍走 Agent 工具循环
71
+
72
+ Returns:
73
+ 命令执行结果,成功时 data 包含 reply、actions 和 frontend_instructions
74
+ """
75
+ if not context.is_project_open:
76
+ return CommandResult.error("未打开项目,请先使用 'open <path>' 命令打开项目")
77
+
78
+ project_path = context.project_path
79
+
80
+ # 获取用于显示的 Provider 配置(脱敏,用于日志或展示)
81
+ provider_config_display = _get_provider_display()
82
+ if not provider_config_display:
83
+ return CommandResult.error("没有可用的 LLM Provider 配置,请先运行 'setup' 命令配置 AI Provider")
84
+
85
+ # 获取包含完整 API Key 的 Provider 配置(用于实际调用 API)
86
+ provider_config = _get_provider_with_key()
87
+ if not provider_config:
88
+ return CommandResult.error("无法获取 Provider 配置信息")
89
+
90
+ # 创建 AI 对话编排器,负责与 AI 模型通信
91
+ orchestrator = AIChatOrchestrator(provider_config)
92
+
93
+ from app.cli.shell.commands.ai.interaction import build_context_data
94
+
95
+ # 构建上下文数据:提取项目中的 schema 信息作为 AI 的上下文
96
+ context_data = build_context_data(message, context)
97
+ context_nodes = context_data.get("context", {}).get("selectedNodes", [])
98
+
99
+ # 创建 spinner 控制器(仅在交互模式下启用)
100
+ spinner = SpinnerController() if interactive else None
101
+
102
+ # 包装回调函数,在调用前暂停 spinner,调用后恢复 spinner
103
+ # 避免 spinner 动画与用户输入提示重叠
104
+ def confirm_actions_wrapper(actions: list[dict], reply: str) -> bool:
105
+ if spinner:
106
+ spinner.pause()
107
+ try:
108
+ return base_confirm_actions(actions, reply)
109
+ finally:
110
+ if spinner:
111
+ spinner.resume()
112
+
113
+ def ambiguity_resolver_wrapper(actions: list[dict], project_path: str) -> bool:
114
+ if spinner:
115
+ spinner.pause()
116
+ try:
117
+ return base_resolve_ambiguities(actions, project_path)
118
+ finally:
119
+ if spinner:
120
+ spinner.resume()
121
+
122
+ # 根据模型上下文窗口计算历史预算(用户输入 > 自动探测 > 全局回退)。
123
+ # 4.24: 预算经 compute_token_budgets 按窗裁剪——原 max(cw-8000, 4096) 在
124
+ # cw<12k 时输入+输出预算之和超过窗口,每次请求必超窗
125
+ context_window = resolve_context_window(provider_config)
126
+ max_history_tokens, _output_budget = compute_token_budgets(context_window)
127
+
128
+ # 配置对话选项
129
+ options = ChatOptions(
130
+ history=history or [],
131
+ max_history_tokens=max_history_tokens,
132
+ temperature=0.1,
133
+ enable_interactive=interactive,
134
+ return_frontend_instructions=True,
135
+ confirm_callback=confirm_actions_wrapper if interactive else None,
136
+ ambiguity_resolver=ambiguity_resolver_wrapper if interactive else None,
137
+ agent_mode=agent_mode,
138
+ max_agent_iterations=5,
139
+ canvas_nodes=[],
140
+ )
141
+
142
+ try:
143
+ import asyncio
144
+
145
+ # 收集原始文件内容(仅在交互模式下,用于 diff 对比)。
146
+ # 4.25: AI 本次新建的文件纳入执行前缓存(登记为空内容)——执行后它们
147
+ # 存在而缓存里没有,diff 生成器会跳过,新建文件的变更在摘要里永不显示
148
+ original_files_cache = {}
149
+ if interactive and project_path:
150
+ original_files_cache = _collect_all_config_files(project_path)
151
+
152
+ # 启动 spinner 动画,提示用户 AI 正在处理
153
+ if spinner:
154
+ spinner.start()
155
+
156
+ # 执行 AI 对话(CLI 是同步环境:asyncio.run 创建一次性事件循环并在结束时确保关闭,
157
+ # 不再使用已弃用的 get_event_loop + run_until_complete,避免循环泄漏)
158
+ result = asyncio.run(
159
+ orchestrator.execute_chat(
160
+ message=message,
161
+ project_path=project_path,
162
+ context_nodes=context_nodes,
163
+ options=options,
164
+ )
165
+ )
166
+
167
+ # 停止 spinner 动画
168
+ if spinner:
169
+ spinner.stop()
170
+
171
+ # 处理结果:如果失败则返回错误
172
+ if not result.success:
173
+ error_msg = result.error or "AI 对话失败"
174
+ if interactive:
175
+ print(Formatter.error(f"\n{error_msg}"))
176
+ return CommandResult.error(error_msg)
177
+
178
+ reply = result.reply
179
+ actions = result.actions
180
+
181
+ # 在交互模式下显示 AI 的文本回复
182
+ if interactive:
183
+ if reply:
184
+ print(reply)
185
+
186
+ # 在交互模式下显示 Agent 工具轨迹(仅 Agent 模式有 tool_steps)
187
+ if interactive and agent_mode and result.tool_steps:
188
+ _display_tool_trail(result.tool_steps)
189
+
190
+ # 在交互模式下显示执行结果和 diff。
191
+ # 4.25: AI 本次新建的文件先登记进缓存(空内容)——执行后它们存在而缓存
192
+ # 没有,diff 生成器会跳过,新建文件的变更在摘要里永不显示
193
+ if interactive and actions and project_path is not None:
194
+ for declared in _collect_declared_new_files(result, project_path):
195
+ original_files_cache.setdefault(declared, "")
196
+ _display_execution_results(result, project_path, original_files_cache)
197
+
198
+ return CommandResult.ok(
199
+ reply if not interactive else "",
200
+ data={
201
+ "reply": reply,
202
+ "actions": actions,
203
+ "frontend_instructions": result.frontend_instructions,
204
+ },
205
+ )
206
+
207
+ except Exception as e:
208
+ # 发生异常时确保 spinner 停止,避免动画残留
209
+ if spinner:
210
+ spinner.stop()
211
+
212
+ logger.error(f"AI 对话失败: {e}", exc_info=True)
213
+ error_msg = f"AI 对话失败: {str(e)}"
214
+
215
+ if interactive:
216
+ print(Formatter.error(error_msg))
217
+ # 针对 HTTP 400 错误提供诊断提示
218
+ if "HTTP 400" in str(e) or "Bad Request" in str(e):
219
+ print(Formatter.warning("\n[诊断提示]"))
220
+ print(Formatter.info(" 1. 检查模型名称是否正确(如 qwen-turbo, qwen-max 等)"))
221
+ print(Formatter.info(" 2. 检查 API Key 是否有效"))
222
+ print(Formatter.info(" 3. 检查网络连接是否正常"))
223
+ print(Formatter.info(" 4. 某些模型可能不支持某些参数,尝试切换模型"))
224
+
225
+ return CommandResult.error(error_msg)
@@ -0,0 +1,271 @@
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/executor_utils.py
17
+ """
18
+ @fileoverview AI 对话执行器模块
19
+
20
+ 功能概述:
21
+ - 统一执行 AI 对话,确保 CLI 与 API 使用相同的底层逻辑
22
+ - 支持交互式确认、歧义解析、流式输出与 diff 展示
23
+ - 集成 Spinner 控制器(加载动画)与文件修改前后对比
24
+ - 负责将 AI 返回的动作(如添加约束)应用到项目配置文件中
25
+
26
+ 架构设计:
27
+ - SpinnerController: 独立的线程控制器,在 AI 处理请求时显示旋转动画
28
+ - _get_provider_with_key(): 获取包含完整 API Key 的 Provider 配置
29
+ - _get_provider_display(): 获取用于显示的 Provider 配置(API Key 脱敏)
30
+ - _collect_all_config_files(): 在执行 AI 前收集所有配置文件内容,用于后续 diff 对比
31
+ - _generate_diff(): 使用 Python 标准库 difflib 生成统一差异格式(unified diff)
32
+ - _show_diff_summary(): 显示修改摘要(新增/删除/修改)并询问是否查看详细 diff
33
+ - _display_detailed_diff(): 使用颜色高亮显示详细的 diff 内容
34
+ - _display_execution_results(): 主入口,根据是否有文件修改决定如何展示结果
35
+ - execute_ai_chat(): 对外暴露的统一执行函数,协调所有子模块完成一次 AI 对话
36
+
37
+ 输入示例:
38
+ result = execute_ai_chat(
39
+ message="在 users 表的 email 列添加非空约束",
40
+ context=project_context,
41
+ interactive=True,
42
+ history=[],
43
+ )
44
+
45
+ 输出示例:
46
+ CommandResult(
47
+ success=True,
48
+ message="",
49
+ data={
50
+ "reply": "已为您添加非空约束...",
51
+ "actions": [...],
52
+ "frontend_instructions": [...],
53
+ },
54
+ )
55
+ """
56
+
57
+ import logging
58
+ import threading
59
+ import time
60
+ from pathlib import Path
61
+
62
+ from app.cli.shell.commands.ai.utils import mask_api_key
63
+ from app.cli.shell.config_storage import get_cli_config
64
+ from app.cli.shell.formatter import Colors, Formatter
65
+ from app.shared.services.llm.config.models import AIProvider
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+
70
+ class SpinnerController:
71
+ """Spinner 控制器,支持暂停和恢复。
72
+
73
+ 在 AI 处理请求期间,在终端显示一个旋转的动画字符,
74
+ 提示用户程序正在工作中。支持在需要用户交互时暂停动画。
75
+
76
+ Attributes:
77
+ _stop_event: 线程事件,用于通知 spinner 线程停止
78
+ _pause_event: 线程事件,用于通知 spinner 线程暂停
79
+ _thread: spinner 后台线程实例
80
+ _chars: 旋转动画使用的 Unicode 字符列表
81
+ """
82
+
83
+ def __init__(self) -> None:
84
+ self._stop_event = threading.Event()
85
+ self._pause_event = threading.Event()
86
+ self._thread: threading.Thread | None = None
87
+ # Unicode Braille 点字图案,用于终端旋转动画
88
+ self._chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
89
+
90
+ def start(self) -> None:
91
+ """启动 spinner 动画。
92
+
93
+ 创建并启动一个后台守护线程,循环显示旋转字符。
94
+ """
95
+ self._stop_event.clear()
96
+ self._pause_event.clear()
97
+ self._thread = threading.Thread(target=self._run, daemon=True)
98
+ self._thread.start()
99
+
100
+ def stop(self) -> None:
101
+ """停止 spinner 动画。
102
+
103
+ 设置停止标志,等待线程结束,并清除终端上的 spinner 残留。
104
+ """
105
+ self._stop_event.set()
106
+ self._pause_event.set() # 确保线程能退出(如果正处于暂停状态)
107
+ if self._thread:
108
+ self._thread.join(timeout=1)
109
+ # 清除 spinner 行:用空格覆盖并回车
110
+ print(f"\r{' ' * 20}\r", end="", flush=True)
111
+
112
+ def pause(self) -> None:
113
+ """暂停 spinner(用于显示确认提示等需要用户输入的场景)。"""
114
+ self._pause_event.set()
115
+ # 清除当前 spinner 行,避免与后续输出重叠
116
+ print(f"\r{' ' * 20}\r", end="", flush=True)
117
+
118
+ def resume(self) -> None:
119
+ """恢复 spinner 动画。"""
120
+ self._pause_event.clear()
121
+
122
+ def _run(self) -> None:
123
+ """spinner 主循环。
124
+
125
+ 后台线程的运行方法,循环显示旋转字符直到收到停止信号。
126
+ """
127
+ i = 0
128
+ while not self._stop_event.is_set():
129
+ if not self._pause_event.is_set():
130
+ char = self._chars[i % len(self._chars)]
131
+ print(f"\r{Formatter.colorize('AI> ', Colors.CYAN)}{char}", end="", flush=True)
132
+ i += 1
133
+ time.sleep(0.1)
134
+
135
+
136
+ def _get_provider_with_key() -> AIProvider | None:
137
+ """
138
+ 获取包含完整 API Key 的 Provider 配置。
139
+
140
+ 从 CLI 配置中读取当前激活的 Provider(已通过 ConfigLoader 解密 api_key)。
141
+
142
+ Returns:
143
+ AIProvider 实例,如果未配置则返回 None
144
+ """
145
+ cli_config = get_cli_config()
146
+ provider = cli_config.get_active_provider()
147
+
148
+ if provider and provider.api_key:
149
+ return provider
150
+ return None
151
+
152
+
153
+ def _get_provider_display() -> dict[str, object] | None:
154
+ """获取用于显示的 Provider 配置(API Key 脱敏)。
155
+
156
+ 将 API Key 替换为掩码形式(如 abc123...xyz),避免在终端泄露密钥。
157
+
158
+ Returns:
159
+ 脱敏后的 Provider 信息字典,如果未配置则返回 None
160
+ """
161
+ cli_config = get_cli_config()
162
+ provider = cli_config.get_active_provider()
163
+
164
+ if provider and provider.api_key:
165
+ # 统一使用 AIProvider 的 type 字段(ProviderType 枚举)
166
+ provider_type = provider.type.value if hasattr(provider.type, "value") else str(provider.type)
167
+ return {
168
+ "id": provider.id,
169
+ "provider": provider_type,
170
+ "api_key": mask_api_key(provider.api_key),
171
+ "base_url": provider.base_url,
172
+ "model": provider.model,
173
+ }
174
+ return None
175
+
176
+
177
+ def _collect_all_config_files(project_path: str) -> dict[str, str]:
178
+ """收集项目所有配置文件的内容(用于 diff 对比)。
179
+
180
+ 在执行 AI 操作前调用,保存 schemas、constraints 和根配置文件的原始内容。
181
+ AI 执行后,通过对比原始内容与修改后内容生成 diff。
182
+
183
+ Args:
184
+ project_path: 项目根目录路径
185
+
186
+ Returns:
187
+ 字典,键为文件绝对路径,值为文件原始内容字符串
188
+ """
189
+ files_content = {}
190
+ project = Path(project_path)
191
+
192
+ # 收集 schemas 目录下的所有 .yaml 文件
193
+ schemas_dir = project / "schemas"
194
+ if schemas_dir.exists():
195
+ for sf in schemas_dir.glob("*.yaml"):
196
+ try:
197
+ files_content[str(sf)] = sf.read_text(encoding="utf-8")
198
+ except Exception:
199
+ logger.error("读取 schema 文件失败", exc_info=True)
200
+
201
+ # 收集 constraints 目录下的所有 .constraint.yaml 文件
202
+ constraints_dir = project / "constraints"
203
+ if constraints_dir.exists():
204
+ for cf in constraints_dir.glob("*.constraint.yaml"):
205
+ try:
206
+ files_content[str(cf)] = cf.read_text(encoding="utf-8")
207
+ except Exception:
208
+ logger.error("读取 constraint 文件失败", exc_info=True)
209
+
210
+ # 4.25: 收集范围对齐 V2 目录结构——regex/、transforms/ 目录变更不在原范围,
211
+ # AI 建正则/转换后的变更摘要永不显示(用户看不到 AI 到底改了什么)
212
+ regex_dir = project / "regex"
213
+ if regex_dir.exists():
214
+ for rf in regex_dir.glob("*.regex.yaml"):
215
+ try:
216
+ files_content[str(rf)] = rf.read_text(encoding="utf-8")
217
+ except Exception:
218
+ logger.error("读取 regex 文件失败", exc_info=True)
219
+
220
+ transforms_dir = project / "transforms"
221
+ if transforms_dir.exists():
222
+ for tf in transforms_dir.glob("*.transform.yaml"):
223
+ try:
224
+ files_content[str(tf)] = tf.read_text(encoding="utf-8")
225
+ except Exception:
226
+ logger.error("读取 transform 文件失败", exc_info=True)
227
+
228
+ # 收集项目根目录下的核心配置文件
229
+ for config_file in ["project.precis.yaml", "patterns.precis.yaml"]:
230
+ config_path = project / config_file
231
+ if config_path.exists():
232
+ try:
233
+ files_content[str(config_path)] = config_path.read_text(encoding="utf-8")
234
+ except Exception:
235
+ logger.error("读取配置文件失败", exc_info=True)
236
+
237
+ return files_content
238
+
239
+
240
+ def _collect_declared_new_files(result: object, project_path: str) -> list[str]:
241
+ """4.25: 从 AI 执行结果/动作声明中提取本次新建的配置文件路径。
242
+
243
+ 扫描约束/正则/转换动作声明的 ID,按 V2 目录约定推导文件路径(文件此时
244
+ 可能尚未写盘——调用方仅用于登记缓存键,路径不存在即为"新建")。
245
+ """
246
+ project = Path(project_path)
247
+ candidates: list[str] = []
248
+ actions: list[dict] = []
249
+ # 兼容 ChatAgentResult 形状(.actions)与裸 dict({"actions": [...]})
250
+ if isinstance(result, dict):
251
+ actions = result.get("actions", []) or []
252
+ else:
253
+ actions = getattr(result, "actions", None) or []
254
+
255
+ for action in actions:
256
+ if not isinstance(action, dict):
257
+ continue
258
+ action_type = str(action.get("actionType", ""))
259
+ spec = action.get("constraintSpec") or action.get("regexSpec") or action.get("transformSpec") or {}
260
+ if not isinstance(spec, dict):
261
+ continue
262
+ raw_id = spec.get("constraintId") or spec.get("regexId") or spec.get("transformId") or spec.get("id")
263
+ if not raw_id or not isinstance(raw_id, str):
264
+ continue
265
+ if action_type.startswith("ADD_CONSTRAINT"):
266
+ candidates.append(str(project / "constraints" / f"{raw_id}.constraint.yaml"))
267
+ elif action_type.startswith("ADD_REGEX"):
268
+ candidates.append(str(project / "regex" / f"{raw_id}.regex.yaml"))
269
+ elif action_type.startswith("ADD_TRANSFORM"):
270
+ candidates.append(str(project / "transforms" / f"{raw_id}.transform.yaml"))
271
+ return candidates