sqlseed-ai 0.2.5__tar.gz → 0.2.6__tar.gz

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 (146) hide show
  1. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/.gitignore +10 -0
  2. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/PKG-INFO +26 -8
  3. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/README.md +25 -7
  4. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/README.zh-CN.md +22 -8
  5. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_hardware.py +65 -14
  6. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_caller.py +2 -0
  7. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_streaming.py +8 -3
  8. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/config.py +6 -1
  9. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/mcp.py +25 -6
  10. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/refiner.py +4 -3
  11. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_hardware.py +154 -4
  12. sqlseed_ai-0.2.6/tests/test_analyzer_client_lifecycle.py +122 -0
  13. sqlseed_ai-0.2.6/tests/test_http_probe_lifecycle.py +66 -0
  14. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_mcp.py +40 -1
  15. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_refiner_table_boundaries.py +88 -9
  16. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/LICENSE +0 -0
  17. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/pyproject.toml +0 -0
  18. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/__init__.py +0 -0
  19. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_client.py +0 -0
  20. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_generator_names.py +0 -0
  21. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_json_utils.py +0 -0
  22. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_model_selector.py +0 -0
  23. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_prompts.py +0 -0
  24. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_tools.py +0 -0
  25. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/ai_mediator.py +0 -0
  26. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/__init__.py +0 -0
  27. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_context.py +0 -0
  28. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_json_parser.py +0 -0
  29. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_tool_calling.py +0 -0
  30. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/__init__.py +0 -0
  31. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/_check_inference.py +0 -0
  32. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/_cross_column_checks.py +0 -0
  33. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/orchestrator.py +0 -0
  34. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/time_budget.py +0 -0
  35. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/cli/__init__.py +0 -0
  36. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/cli/ai_commands.py +0 -0
  37. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/__init__.py +0 -0
  38. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/builtin_violations.py +0 -0
  39. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/matrix.py +0 -0
  40. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/registry.py +0 -0
  41. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/errors.py +0 -0
  42. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/examples.py +0 -0
  43. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/exceptions.py +0 -0
  44. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/__init__.py +0 -0
  45. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/_client.py +0 -0
  46. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/_llm_call.py +0 -0
  47. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/candidate_validation.py +0 -0
  48. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/context_detector.py +0 -0
  49. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/degrader.py +0 -0
  50. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/diff_learner.py +0 -0
  51. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/failure_classifier.py +0 -0
  52. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/level1_subgraph_healer.py +0 -0
  53. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/level2_column_healer.py +0 -0
  54. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/level3_compact_healer.py +0 -0
  55. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/models.py +0 -0
  56. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/orchestrator.py +0 -0
  57. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/oscillation.py +0 -0
  58. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/post_repair.py +0 -0
  59. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/subgraph.py +0 -0
  60. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/__init__.py +0 -0
  61. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/executor.py +0 -0
  62. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/models.py +0 -0
  63. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/pipeline.py +0 -0
  64. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/strategies.py +0 -0
  65. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/runtime.py +0 -0
  66. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/__init__.py +0 -0
  67. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/composite_fk.py +0 -0
  68. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/cross_column.py +0 -0
  69. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/dialect_parser.py +0 -0
  70. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/main.py +0 -0
  71. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/models.py +0 -0
  72. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/schema_snapshot.py +0 -0
  73. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/shadow_fk_scan.py +0 -0
  74. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/single_column.py +0 -0
  75. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/.pylintrc +0 -0
  76. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/__init__.py +0 -0
  77. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/conftest.py +0 -0
  78. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/__init__.py +0 -0
  79. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/conftest.py +0 -0
  80. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/scenario_helpers.py +0 -0
  81. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_context_detector.py +0 -0
  82. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_failure_classifier.py +0 -0
  83. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_heal_orchestrator_real.py +0 -0
  84. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_level1_subgraph_healer_real.py +0 -0
  85. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_level2_column_healer_real.py +0 -0
  86. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_level2_context_builder.py +0 -0
  87. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_level3_compact_healer_real.py +0 -0
  88. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_llm_availability.py +0 -0
  89. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/healer/test_llm_call_boundary.py +0 -0
  90. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/http_helpers.py +0 -0
  91. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/property/__init__.py +0 -0
  92. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/property/test_matrix_completeness.py +0 -0
  93. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/schema_helpers.py +0 -0
  94. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_analyzer_streaming.py +0 -0
  95. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_caller.py +0 -0
  96. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_client.py +0 -0
  97. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_commands.py +0 -0
  98. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_config.py +0 -0
  99. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_errors.py +0 -0
  100. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_json_utils.py +0 -0
  101. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_mediator.py +0 -0
  102. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_model_selector.py +0 -0
  103. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_plugin.py +0 -0
  104. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_plugin_init.py +0 -0
  105. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_preserve_names.py +0 -0
  106. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_prompts_tools.py +0 -0
  107. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_ai_tool_calling.py +0 -0
  108. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_auto_heal_orchestrator.py +0 -0
  109. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_auto_heal_sonar_boundaries.py +0 -0
  110. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_auto_heal_time_budget.py +0 -0
  111. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_cli_auto_heal.py +0 -0
  112. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_cli_input_contract.py +0 -0
  113. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_cli_suggestion_diagnostics.py +0 -0
  114. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_cli_suggestion_identity.py +0 -0
  115. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_client_loopback.py +0 -0
  116. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_contracts_builtin.py +0 -0
  117. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_contracts_matrix.py +0 -0
  118. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_contracts_registry.py +0 -0
  119. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_candidate_contract.py +0 -0
  120. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_degrader.py +0 -0
  121. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_diff_learner.py +0 -0
  122. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_models.py +0 -0
  123. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_oscillation.py +0 -0
  124. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_post_repair.py +0 -0
  125. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_healer_subgraph.py +0 -0
  126. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_mcp_error_redaction.py +0 -0
  127. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_mcp_stdio.py +0 -0
  128. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_nonweb_ai_boundaries.py +0 -0
  129. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_prompts_p0_p3.py +0 -0
  130. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_quality_error_boundaries.py +0 -0
  131. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_real_llm_environment.py +0 -0
  132. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_refiner.py +0 -0
  133. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_refiner_json_recovery.py +0 -0
  134. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_repair_executor.py +0 -0
  135. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_repair_models.py +0 -0
  136. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_repair_pipeline.py +0 -0
  137. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_repair_strategies.py +0 -0
  138. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_runtime.py +0 -0
  139. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_schema_snapshot.py +0 -0
  140. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_composite_fk.py +0 -0
  141. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_cross_column.py +0 -0
  142. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_dialect_parser.py +0 -0
  143. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_main.py +0 -0
  144. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_models.py +0 -0
  145. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_shadow_fk_scan.py +0 -0
  146. {sqlseed_ai-0.2.5 → sqlseed_ai-0.2.6}/tests/test_validator_single_column.py +0 -0
@@ -59,6 +59,16 @@ dmypy.json
59
59
  *.db
60
60
  *.sqlite
61
61
  *.sqlite3
62
+ # SQLite runtime sidecars
63
+ *.db-wal
64
+ *.db-shm
65
+ *.db-journal
66
+ *.sqlite-wal
67
+ *.sqlite-shm
68
+ *.sqlite-journal
69
+ *.sqlite3-wal
70
+ *.sqlite3-shm
71
+ *.sqlite3-journal
62
72
  .sqlseed_cache/
63
73
  snapshots/
64
74
  .env
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlseed-ai
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Optional LLM schema analysis and contract-driven configuration repair for sqlseed
5
5
  Project-URL: Documentation, https://sunbos.github.io/sqlseed/gemma4-integration/
6
6
  Project-URL: Homepage, https://github.com/sunbos/sqlseed
@@ -46,12 +46,12 @@ backend test; installing the plugin does not perform one.
46
46
 
47
47
  ## Installation
48
48
 
49
- These instructions target version 0.2.5. Check [Releases](https://github.com/sunbos/sqlseed/releases)
49
+ These instructions target version 0.2.6. Check [Releases](https://github.com/sunbos/sqlseed/releases)
50
50
  for publication status; use the source installation below to test an unpublished candidate.
51
51
  Use a Python 3.10+ virtual environment:
52
52
 
53
53
  ```bash
54
- python -m pip install "sqlseed-ai==0.2.5"
54
+ python -m pip install "sqlseed-ai==0.2.6"
55
55
  ```
56
56
 
57
57
  Core 0.2.4 and older lack the shared diagnostic interfaces required by this version.
@@ -136,7 +136,7 @@ actual generated values and database constraints.
136
136
  The AI MCP entry point requires the `mcp` extra:
137
137
 
138
138
  ```bash
139
- python -m pip install "sqlseed-ai[mcp]==0.2.5"
139
+ python -m pip install "sqlseed-ai[mcp]==0.2.6"
140
140
  mcp-server-sqlseed-ai
141
141
  ```
142
142
 
@@ -189,13 +189,31 @@ Strict tool calling also rejects arrays, scalars and `null` arguments as `invali
189
189
  compatibility mode may still fall back to response text. Parsed suggestions still
190
190
  require scope and rule validation.
191
191
 
192
- AI configuration caches include schema hashes. Schema changes invalidate cached
193
- suggestions; `--no-cache` bypasses them. Malformed cache metadata or configuration
194
- containers are treated as cache misses. Review model output before writing data.
192
+ Single-table suggestion caches use a versioned hash of sorted column names, encoded
193
+ as a JSON array so names containing delimiters remain distinct. The check detects
194
+ added, removed, or renamed columns; column order, types, and constraints are not
195
+ included. Caches using the older delimiter encoding are ignored and regenerated.
196
+ Use `--no-cache` to analyze again without cached suggestions. This cache check is
197
+ separate from AutoHeal's full schema fingerprint. Malformed cache metadata
198
+ or configuration containers are treated as cache misses. Review model output before
199
+ writing data.
200
+
201
+ ## Hardware estimates on macOS
202
+
203
+ The AI MCP model list distinguishes Apple unified memory, Intel shared graphics
204
+ memory, and dedicated GPU memory. Apple unified RAM is not reported as dedicated
205
+ VRAM or added to system RAM. `unified_memory_budget_gb` is a static heuristic:
206
+ `max(0, min(total_ram_gb * 0.75, total_ram_gb - 4))`, reserving at least 4 GiB or
207
+ 25% for the system. It is not measured free memory or a Metal allocation limit.
208
+
209
+ For an identified Apple GPU on macOS, model screening compares that budget with
210
+ both existing minimum RAM and VRAM estimates and reports at most `capable`.
211
+ This does not verify Metal acceleration, backend/model support, or successful
212
+ inference; loaded applications and context size can require more memory.
195
213
 
196
214
  ## Requirements
197
215
 
198
- These metadata requirements apply to version 0.2.5 and its source candidates.
216
+ These metadata requirements apply to version 0.2.6 and its source candidates.
199
217
  Use local Core and plugins together when developing from source.
200
218
 
201
219
  - Python `>=3.10`
@@ -14,12 +14,12 @@ backend test; installing the plugin does not perform one.
14
14
 
15
15
  ## Installation
16
16
 
17
- These instructions target version 0.2.5. Check [Releases](https://github.com/sunbos/sqlseed/releases)
17
+ These instructions target version 0.2.6. Check [Releases](https://github.com/sunbos/sqlseed/releases)
18
18
  for publication status; use the source installation below to test an unpublished candidate.
19
19
  Use a Python 3.10+ virtual environment:
20
20
 
21
21
  ```bash
22
- python -m pip install "sqlseed-ai==0.2.5"
22
+ python -m pip install "sqlseed-ai==0.2.6"
23
23
  ```
24
24
 
25
25
  Core 0.2.4 and older lack the shared diagnostic interfaces required by this version.
@@ -104,7 +104,7 @@ actual generated values and database constraints.
104
104
  The AI MCP entry point requires the `mcp` extra:
105
105
 
106
106
  ```bash
107
- python -m pip install "sqlseed-ai[mcp]==0.2.5"
107
+ python -m pip install "sqlseed-ai[mcp]==0.2.6"
108
108
  mcp-server-sqlseed-ai
109
109
  ```
110
110
 
@@ -157,13 +157,31 @@ Strict tool calling also rejects arrays, scalars and `null` arguments as `invali
157
157
  compatibility mode may still fall back to response text. Parsed suggestions still
158
158
  require scope and rule validation.
159
159
 
160
- AI configuration caches include schema hashes. Schema changes invalidate cached
161
- suggestions; `--no-cache` bypasses them. Malformed cache metadata or configuration
162
- containers are treated as cache misses. Review model output before writing data.
160
+ Single-table suggestion caches use a versioned hash of sorted column names, encoded
161
+ as a JSON array so names containing delimiters remain distinct. The check detects
162
+ added, removed, or renamed columns; column order, types, and constraints are not
163
+ included. Caches using the older delimiter encoding are ignored and regenerated.
164
+ Use `--no-cache` to analyze again without cached suggestions. This cache check is
165
+ separate from AutoHeal's full schema fingerprint. Malformed cache metadata
166
+ or configuration containers are treated as cache misses. Review model output before
167
+ writing data.
168
+
169
+ ## Hardware estimates on macOS
170
+
171
+ The AI MCP model list distinguishes Apple unified memory, Intel shared graphics
172
+ memory, and dedicated GPU memory. Apple unified RAM is not reported as dedicated
173
+ VRAM or added to system RAM. `unified_memory_budget_gb` is a static heuristic:
174
+ `max(0, min(total_ram_gb * 0.75, total_ram_gb - 4))`, reserving at least 4 GiB or
175
+ 25% for the system. It is not measured free memory or a Metal allocation limit.
176
+
177
+ For an identified Apple GPU on macOS, model screening compares that budget with
178
+ both existing minimum RAM and VRAM estimates and reports at most `capable`.
179
+ This does not verify Metal acceleration, backend/model support, or successful
180
+ inference; loaded applications and context size can require more memory.
163
181
 
164
182
  ## Requirements
165
183
 
166
- These metadata requirements apply to version 0.2.5 and its source candidates.
184
+ These metadata requirements apply to version 0.2.6 and its source candidates.
167
185
  Use local Core and plugins together when developing from source.
168
186
 
169
187
  - Python `>=3.10`
@@ -3,7 +3,7 @@
3
3
  [English](https://github.com/sunbos/sqlseed/blob/main/plugins/sqlseed-ai/README.md) |
4
4
  **[中文](https://github.com/sunbos/sqlseed/blob/main/plugins/sqlseed-ai/README.zh-CN.md)**
5
5
 
6
- [sqlseed](https://sunbos.github.io/sqlseed/) 的可选 LLM Schema 分析与契约驱动配置修复插件。
6
+ [sqlseed](https://sunbos.github.io/sqlseed/zh-CN/) 的可选 LLM Schema 分析与契约驱动配置修复插件。
7
7
  提供列规则建议、配置校验与修复,以及模板候选值生成。接受的配置可交由 Core 离线执行。
8
8
 
9
9
  支持 Google AI Studio、LM Studio、Ollama 和 OpenAI-compatible API 后端。
@@ -11,10 +11,10 @@
11
11
 
12
12
  ## 安装
13
13
 
14
- 本文安装说明对应 0.2.5 版本;发布状态以 [Releases](https://github.com/sunbos/sqlseed/releases) 为准。测试尚未发布的候选版本时,使用下方的源码安装方式。请先创建并激活 Python 3.10+ 虚拟环境:
14
+ 本文安装说明对应 0.2.6 版本;发布状态以 [Releases](https://github.com/sunbos/sqlseed/releases) 为准。测试尚未发布的候选版本时,使用下方的源码安装方式。请先创建并激活 Python 3.10+ 虚拟环境:
15
15
 
16
16
  ```bash
17
- python -m pip install "sqlseed-ai==0.2.5"
17
+ python -m pip install "sqlseed-ai==0.2.6"
18
18
  ```
19
19
 
20
20
  Core 0.2.4 及更早版本缺少本版本使用的共享诊断接口。
@@ -91,7 +91,7 @@ native/custom 方法、实际生成值及依赖数据库状态的约束需另行
91
91
  AI MCP 入口要求本包的 `mcp` extra:
92
92
 
93
93
  ```bash
94
- python -m pip install "sqlseed-ai[mcp]==0.2.5"
94
+ python -m pip install "sqlseed-ai[mcp]==0.2.6"
95
95
  mcp-server-sqlseed-ai
96
96
  ```
97
97
 
@@ -194,7 +194,10 @@ Ollama 使用 JSON 对象模式。服务明确拒绝该格式时,只进行一
194
194
 
195
195
  ### 文件缓存
196
196
 
197
- AI 配置缓存包含 schema hash,结构变化会使旧建议失效;`--no-cache` 跳过缓存。
197
+ 单表建议缓存使用带版本标识的 schema hash,将排序后的列名编码为 JSON 数组,
198
+ 避免含分隔符的不同列名集合混淆。校验覆盖列的增删和重命名,不包含列顺序、类型或约束。
199
+ 旧分隔符编码的缓存会被忽略并重新生成;可用 `--no-cache` 跳过缓存重新分析。
200
+ 此处的缓存校验与 AutoHeal 使用的完整 schema 指纹不同。
198
201
  缓存元数据或配置容器类型无效时,按缓存未命中重新分析。
199
202
  默认路径为 macOS 的 `~/Library/Caches/sqlseed/ai_configs/`、Linux 的
200
203
  `$XDG_CACHE_HOME/sqlseed/ai_configs/`(未设置时为 `~/.cache/sqlseed/ai_configs/`),
@@ -215,9 +218,20 @@ AI 配置缓存包含 schema hash,结构变化会使旧建议失效;`--no-ca
215
218
  CLI 命令另由 `sqlseed.cli_commands` entry point 注册。本插件不实现 provider 或
216
219
  column-mapper 注册 hooks,也不要求 Core 导入 AI 实现。
217
220
 
221
+ ## macOS 硬件估算
222
+
223
+ AI MCP 模型列表区分 Apple 统一内存、Intel 共享显存与独立显存。Apple 统一内存
224
+ 不会被报告为独立显存,也不会与系统 RAM 相加。`unified_memory_budget_gb` 是静态
225
+ 启发式预算:`max(0, min(total_ram_gb * 0.75, total_ram_gb - 4))`,为系统预留
226
+ 至少 4 GiB 或 25% 内存。它不是实测可用内存,也不是 Metal 分配上限。
227
+
228
+ 仅在 macOS 上识别出 Apple GPU 后,模型筛选才使用该预算与既有最低 RAM、VRAM
229
+ 估算同时比较,最高返回 `capable`。该结果不代表已验证 Metal 加速、后端或模型支持,
230
+ 也不保证推理成功;其他应用占用和上下文大小可能增加实际内存需求。
231
+
218
232
  ## 依赖
219
233
 
220
- 以下为 0.2.5 版本及其源码候选的依赖要求。源码开发时请在同一次解析中安装本地 Core 和插件。
234
+ 以下为 0.2.6 版本及其源码候选的依赖要求。源码开发时请在同一次解析中安装本地 Core 和插件。
221
235
 
222
236
  - Python `>=3.10`
223
237
  - `sqlseed>=0.2.5.dev0,<0.3`
@@ -228,8 +242,8 @@ column-mapper 注册 hooks,也不要求 Core 导入 AI 实现。
228
242
  - 可选 `mcp` extra:`mcp>=1.0,<2`
229
243
  - 实际模型请求需要已配置且可达的后端
230
244
 
231
- 更多信息见[AI 集成指南](https://sunbos.github.io/sqlseed/gemma4-integration.zh-CN/)、
232
- [升级说明](https://sunbos.github.io/sqlseed/migration.zh-CN/)和
245
+ 更多信息见[AI 集成指南](https://sunbos.github.io/sqlseed/zh-CN/gemma4-integration/)、
246
+ [升级说明](https://sunbos.github.io/sqlseed/zh-CN/migration/)和
233
247
  [配置源码](https://github.com/sunbos/sqlseed/blob/main/plugins/sqlseed-ai/src/sqlseed_ai/config.py)。
234
248
 
235
249
  许可证:[AGPL-3.0-or-later](https://github.com/sunbos/sqlseed/blob/main/LICENSE)。
@@ -11,8 +11,10 @@ from __future__ import annotations
11
11
  import ctypes
12
12
  import json
13
13
  import platform
14
+ import re
14
15
  import subprocess
15
16
  import time
17
+ from math import isfinite
16
18
  from typing import Any, NamedTuple
17
19
 
18
20
  from sqlseed._utils.logger import get_logger
@@ -200,8 +202,34 @@ def _detect_gpu_nvidia() -> list[dict[str, Any]]:
200
202
  return []
201
203
 
202
204
 
205
+ def _video_memory_mb(value: object) -> int:
206
+ """Parse a profiler memory quantity without trusting malformed card fields."""
207
+ if not isinstance(value, str):
208
+ return 0
209
+ if (match := re.fullmatch(r"(\d+(?:\.\d+)?)\s*(MB|GB)", value.strip(), re.IGNORECASE)) is None:
210
+ return 0
211
+ size_mb = float(match[1]) * (1024 if match[2].upper() == "GB" else 1)
212
+ return int(size_mb) if isfinite(size_mb) else 0
213
+
214
+
215
+ def _macos_gpu_memory(gpu_info: dict[str, object], vendor: str) -> tuple[str, int]:
216
+ """Separate shared and dedicated video memory from Apple unified RAM."""
217
+ if vendor == "apple":
218
+ return "unified", 0
219
+ memory_type = "shared" if "spdisplays_vram_shared" in gpu_info else "dedicated"
220
+ vram_mb = next(
221
+ (
222
+ size
223
+ for key in ("spdisplays_vram", "spdisplays_vram_shared", "_spdisplays_vram")
224
+ if (size := _video_memory_mb(gpu_info.get(key))) > 0
225
+ ),
226
+ 0,
227
+ )
228
+ return memory_type, vram_mb
229
+
230
+
203
231
  def _detect_gpu_macos() -> list[dict[str, Any]]:
204
- """Detect Apple Silicon GPU via system_profiler. macOS only."""
232
+ """Detect Apple, Intel and discrete GPUs via macOS system_profiler."""
205
233
  try:
206
234
  result = subprocess.run(
207
235
  ["system_profiler", "SPDisplaysDataType", "-json"],
@@ -214,26 +242,30 @@ def _detect_gpu_macos() -> list[dict[str, Any]]:
214
242
  return []
215
243
 
216
244
  data = json.loads(result.stdout)
245
+ if not isinstance(data, dict):
246
+ return []
217
247
  displays = data.get("SPDisplaysDataType", [])
248
+ if not isinstance(displays, list):
249
+ return []
218
250
  gpus: list[dict[str, Any]] = []
219
251
  for gpu_info in displays:
252
+ if not isinstance(gpu_info, dict):
253
+ continue
220
254
  name = gpu_info.get("sppci_model", "Unknown GPU")
221
- vram_str = gpu_info.get("spdisplays_vram", "")
222
- vram_mb = 0
223
- if vram_str:
224
- parts = vram_str.split()
225
- if len(parts) >= 2:
226
- val = int(parts[0])
227
- unit = parts[1].upper()
228
- vram_mb = val * 1024 if "GB" in unit else val
255
+ if not isinstance(name, str):
256
+ name = "Unknown GPU"
257
+ vendor_label = str(gpu_info.get("spdisplays_vendor") or name).lower()
258
+ vendor = next((v for v in ("apple", "intel", "amd", "nvidia") if v in vendor_label), "unknown")
259
+ memory_type, vram_mb = _macos_gpu_memory(gpu_info, vendor)
229
260
 
230
261
  gpus.append(
231
262
  {
232
263
  "name": name,
233
264
  "vram_total_mb": vram_mb,
234
- "vram_free_mb": 0, # Apple Silicon uses unified memory; discrete VRAM is always 0
265
+ "vram_free_mb": 0, # system_profiler does not report free VRAM
235
266
  "vram_total_gb": round(vram_mb / 1024, 1),
236
- "vendor": "apple",
267
+ "vendor": vendor,
268
+ "memory_type": memory_type,
237
269
  }
238
270
  )
239
271
  return gpus
@@ -252,6 +284,13 @@ def _detect_gpus() -> list[dict[str, Any]]:
252
284
  return []
253
285
 
254
286
 
287
+ def _has_apple_unified_memory(hw: dict[str, Any]) -> bool:
288
+ """Require an identified Apple GPU on macOS, including under Rosetta."""
289
+ return hw.get("platform", {}).get("system") == "Darwin" and any(
290
+ gpu.get("vendor") == "apple" and gpu.get("memory_type") == "unified" for gpu in hw.get("gpus", [])
291
+ )
292
+
293
+
255
294
  # ── Public API ───────────────────────────────────────────────────────
256
295
 
257
296
 
@@ -262,7 +301,8 @@ def detect_hardware() -> dict[str, Any]:
262
301
  platform: {system, release, machine}
263
302
  ram: {total_gb, available_gb}
264
303
  gpus: [{name, vram_total_mb, vram_free_mb, vram_total_gb, vendor, ...}]
265
- max_vram_gb: float (max VRAM across all GPUs, 0 if no GPU)
304
+ max_vram_gb: float (largest reported non-unified VRAM quantity)
305
+ unified_memory_budget_gb: float (heuristic budget, not measured free VRAM)
266
306
  """
267
307
  if _HardwareCache.data is not None:
268
308
  cached_time, cached_result = _HardwareCache.data
@@ -271,9 +311,9 @@ def detect_hardware() -> dict[str, Any]:
271
311
 
272
312
  ram = _detect_system_ram()
273
313
  gpus = _detect_gpus()
274
- max_vram = max((g.get("vram_total_gb", 0) for g in gpus), default=0)
314
+ max_vram = max((g.get("vram_total_gb", 0) for g in gpus if g.get("memory_type") != "unified"), default=0)
275
315
 
276
- result = {
316
+ result: dict[str, Any] = {
277
317
  "platform": {
278
318
  "system": platform.system(),
279
319
  "release": platform.release(),
@@ -282,7 +322,13 @@ def detect_hardware() -> dict[str, Any]:
282
322
  "ram": ram,
283
323
  "gpus": gpus,
284
324
  "max_vram_gb": max_vram,
325
+ "unified_memory_budget_gb": 0.0,
285
326
  }
327
+ if _has_apple_unified_memory(result):
328
+ # A static screening heuristic, not Metal's runtime allocation limit.
329
+ # Reserve at least 4 GiB / 25% for the OS and other applications.
330
+ total_ram = ram["total_gb"]
331
+ result["unified_memory_budget_gb"] = max(0.0, min(total_ram * 0.75, total_ram - 4.0))
286
332
 
287
333
  _HardwareCache.data = (time.monotonic(), result)
288
334
  logger.info(
@@ -340,6 +386,11 @@ def evaluate_model_status(
340
386
  return "recommended"
341
387
  if max_vram >= req.min_vram_gb:
342
388
  return "capable"
389
+ if _has_apple_unified_memory(hw):
390
+ budget = hw.get("unified_memory_budget_gb", 0)
391
+ # RAM and GPU allocations share this budget; never add them or grant
392
+ # a recommendation without verifying the backend and actual model.
393
+ return "capable" if budget >= max(req.min_ram_gb, req.min_vram_gb) else "insufficient"
343
394
  if total_ram >= req.min_ram_gb and max_vram == 0:
344
395
  return "cpu_only"
345
396
  if total_ram >= req.min_ram_gb:
@@ -408,6 +408,8 @@ class LLMCallerMixin(_InteractionLoggingMixin):
408
408
  error=str(e),
409
409
  )
410
410
  self._handle_llm_api_exception(e, model, streaming=False)
411
+ finally:
412
+ client.close()
411
413
 
412
414
  return self._parse_llm_completion(
413
415
  response,
@@ -8,6 +8,7 @@ request strategy (tool calling vs JSON mode vs text mode).
8
8
  from __future__ import annotations
9
9
 
10
10
  import time
11
+ from contextlib import ExitStack
11
12
  from typing import TYPE_CHECKING, Any, NoReturn
12
13
 
13
14
  from sqlseed_ai._client import APIConnectionError, APIError, APITimeoutError, get_openai_client
@@ -217,15 +218,17 @@ class StreamingHandlerMixin(_InteractionLoggingMixin):
217
218
  raise RuntimeError("AIConfig must be initialized before calling LLM")
218
219
  client = get_openai_client(self._config)
219
220
  start_time = time.time()
220
-
221
- if on_progress:
222
- on_progress("connecting", {"model": model or self._config.model})
221
+ resources = ExitStack()
222
+ resources.callback(client.close)
223
223
 
224
224
  try:
225
+ if on_progress:
226
+ on_progress("connecting", {"model": model or self._config.model})
225
227
  kwargs = self._build_llm_kwargs(stream=True, model=model)
226
228
  kwargs["messages"] = messages
227
229
 
228
230
  stream = self._create_streaming_response(client, kwargs, strict_json=strict_json)
231
+ resources.callback(stream.close)
229
232
 
230
233
  content, token_count = self._collect_stream_chunks(stream, on_progress, strict_json=strict_json)
231
234
 
@@ -285,6 +288,8 @@ class StreamingHandlerMixin(_InteractionLoggingMixin):
285
288
  error=str(e),
286
289
  )
287
290
  self._handle_llm_api_exception(e, model, streaming=True)
291
+ finally:
292
+ resources.close()
288
293
 
289
294
  def _create_streaming_response(self, client: Any, kwargs: dict[str, Any], *, strict_json: bool) -> Any:
290
295
  """Apply local JSON sampling without changing cloud streaming dispatch."""
@@ -12,6 +12,7 @@ import json
12
12
  import os
13
13
  import re
14
14
  import time
15
+ import urllib.error
15
16
  import urllib.request
16
17
  from enum import Enum
17
18
  from typing import Any, Literal, cast
@@ -317,7 +318,9 @@ class AIConfig(BaseModel):
317
318
  models = [str(m.get("id")) for m in data.get("data", []) if m.get("id")]
318
319
  self._all_models_cache = (time.monotonic(), models)
319
320
  return models
320
- except (OSError, ValueError, KeyError):
321
+ except (OSError, ValueError, KeyError) as error:
322
+ if isinstance(error, urllib.error.HTTPError):
323
+ error.close()
321
324
  logger.debug("Could not auto-detect local models", backend=self.backend.value)
322
325
 
323
326
  # Cache negative result too, to avoid hammering a dead endpoint
@@ -626,6 +629,8 @@ class AIConfig(BaseModel):
626
629
  return info
627
630
 
628
631
  except (OSError, ValueError, RuntimeError) as e:
632
+ if isinstance(e, urllib.error.HTTPError):
633
+ e.close()
629
634
  logger.debug("Inference speed probe failed", error=str(e))
630
635
  return None
631
636
 
@@ -19,6 +19,7 @@ required", not "online/offline". This package requires an LLM runtime.
19
19
  from __future__ import annotations
20
20
 
21
21
  import json
22
+ import urllib.error
22
23
  import urllib.request
23
24
  from typing import Any
24
25
 
@@ -30,7 +31,12 @@ except ImportError as _exc: # pragma: no cover - import error path
30
31
  raise ImportError("mcp SDK not installed. Install with: pip install 'sqlseed-ai[mcp]'") from _exc
31
32
 
32
33
  from sqlseed_ai import AIBackend, AIConfig, AiConfigRefiner, AISuggestionFailedError, GemmaModel, SchemaAnalyzer
33
- from sqlseed_ai._hardware import MODEL_REQUIREMENTS, detect_hardware, evaluate_model_status
34
+ from sqlseed_ai._hardware import (
35
+ MODEL_REQUIREMENTS,
36
+ _has_apple_unified_memory,
37
+ detect_hardware,
38
+ evaluate_model_status,
39
+ )
34
40
 
35
41
  from sqlseed import ColumnConfig, DataOrchestrator
36
42
  from sqlseed._utils import paths
@@ -97,7 +103,12 @@ def sqlseed_ai_generate_yaml(
97
103
  semantics. For rule-driven offline generation, use the
98
104
  ``sqlseed_generate_yaml`` tool from ``mcp-server-sqlseed`` instead.
99
105
 
100
- Supported backends: google_ai_studio (default), lm_studio, ollama, openai_compat.
106
+ Supported backends: google_ai_studio, lm_studio, ollama, openai_compat.
107
+ Backend selection uses the backend argument, then SQLSEED_AI_BACKEND,
108
+ then recognized URL patterns in SQLSEED_AI_BASE_URL or OPENAI_BASE_URL;
109
+ otherwise it defaults to openai_compat. The base_url argument only overrides
110
+ the endpoint and does not infer the backend. Pass backend explicitly when
111
+ using base_url to select a provider.
101
112
  Returns a YAML string for human review.
102
113
  """
103
114
  try:
@@ -148,7 +159,10 @@ def sqlseed_gemma4_analyze(
148
159
  table structure and recommend data generation configurations. It demonstrates
149
160
  Gemma 4's Native Function Calling feature for the AI Agent track.
150
161
 
151
- Supported backends: google_ai_studio (default), lm_studio, ollama, openai_compat.
162
+ Supported backends: google_ai_studio, lm_studio, ollama, openai_compat.
163
+ Backend selection uses the backend argument, then SQLSEED_AI_BACKEND,
164
+ then recognized URL patterns in SQLSEED_AI_BASE_URL or OPENAI_BASE_URL;
165
+ otherwise it defaults to openai_compat.
152
166
  Supported models: gemma-4-26b-a4b-it (default), gemma-4-31b-it, gemma-4-12b-it,
153
167
  gemma-4-e4b-it, gemma-4-e2b-it.
154
168
  """
@@ -293,8 +307,9 @@ def _check_local_backend(backend_id: str, url: str) -> dict[str, Any]:
293
307
  data = json.loads(resp.read().decode())
294
308
  loaded = [m.get("id", "unknown") for m in data.get("data", []) if m.get("id")]
295
309
  reachable = True
296
- except (OSError, ValueError):
297
- pass
310
+ except (OSError, ValueError) as error:
311
+ if isinstance(error, urllib.error.HTTPError):
312
+ error.close()
298
313
 
299
314
  if reachable and loaded:
300
315
  reason = f"{len(loaded)} model(s) loaded"
@@ -350,12 +365,15 @@ def _build_models(hw: dict[str, Any]) -> list[dict[str, Any]]:
350
365
  for member in GemmaModel:
351
366
  status = evaluate_model_status(member.value, hw)
352
367
  req = MODEL_REQUIREMENTS.get(member.value)
368
+ status_description = _STATUS_ICONS.get(status, status)
369
+ if _has_apple_unified_memory(hw) and status != "cloud_only":
370
+ status_description += "; heuristic unified-memory budget, backend/model support not verified"
353
371
  models.append(
354
372
  {
355
373
  "id": member.value,
356
374
  "display_name": member.display_name,
357
375
  "status": status,
358
- "status_description": _STATUS_ICONS.get(status, status),
376
+ "status_description": status_description,
359
377
  "local_only": member.is_local_only,
360
378
  "requirements": {
361
379
  "min_ram_gb": req.min_ram_gb if req else 0,
@@ -417,6 +435,7 @@ def sqlseed_list_gemma_models() -> dict[str, Any]:
417
435
  "ram": hw["ram"],
418
436
  "gpus": hw["gpus"],
419
437
  "max_vram_gb": hw["max_vram_gb"],
438
+ "unified_memory_budget_gb": hw.get("unified_memory_budget_gb", 0.0),
420
439
  },
421
440
  }
422
441
 
@@ -559,15 +559,16 @@ class AiConfigRefiner:
559
559
  table_name: Name of the table to hash.
560
560
 
561
561
  Returns:
562
- Truncated SHA-256 hex digest (16 chars) of the sorted column names.
562
+ Versioned, truncated SHA-256 digest of unambiguously encoded sorted column names.
563
563
 
564
564
  Raises:
565
565
  ValueError: If the target has no columns to generate, including a missing table.
566
566
  """
567
567
  if not (column_names := orch.get_column_names(table_name)):
568
568
  raise ValueError(f"Table '{table_name}' does not exist or has no columns")
569
- raw = "|".join(sorted(column_names))
570
- return hashlib.sha256(raw.encode()).hexdigest()[:16]
569
+ raw = json.dumps(sorted(column_names), ensure_ascii=True, separators=(",", ":"))
570
+ # Keep legacy delimiter hashes separate even when a column name spells JSON.
571
+ return "columns-v2:" + hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
571
572
 
572
573
  def _validate_config(
573
574
  self,