@caddis/cli 0.1.0 → 0.2.0

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 (364) hide show
  1. package/README.md +29 -16
  2. package/bundles/antigravity-plugin-extras/plugin.json +5 -0
  3. package/bundles/antigravity-plugin-extras/skills/_registry.md +154 -0
  4. package/bundles/antigravity-plugin-extras/skills/agent-md-refactor/SKILL.md +356 -0
  5. package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +517 -0
  6. package/bundles/antigravity-plugin-extras/skills/algorithmic-art/SKILL.md +293 -0
  7. package/bundles/antigravity-plugin-extras/skills/anchor-review/SKILL.md +135 -0
  8. package/bundles/antigravity-plugin-extras/skills/api-client-patterns/SKILL.md +367 -0
  9. package/bundles/antigravity-plugin-extras/skills/architecture-design/SKILL.md +244 -0
  10. package/bundles/antigravity-plugin-extras/skills/architecture-diagram/SKILL.md +156 -0
  11. package/bundles/antigravity-plugin-extras/skills/architecture-document/SKILL.md +321 -0
  12. package/bundles/antigravity-plugin-extras/skills/artifacts-builder/LICENSE.txt +202 -0
  13. package/bundles/antigravity-plugin-extras/skills/artifacts-builder/SKILL.md +74 -0
  14. package/bundles/antigravity-plugin-extras/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  15. package/bundles/antigravity-plugin-extras/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  16. package/bundles/antigravity-plugin-extras/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  17. package/bundles/antigravity-plugin-extras/skills/ascii-art/SKILL.md +144 -0
  18. package/bundles/antigravity-plugin-extras/skills/asking-questions/SKILL.md +81 -0
  19. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/SKILL.md +118 -0
  20. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/cross-service/credential-management.md +450 -0
  21. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/browser/README.md +363 -0
  22. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/code-interpreter/README.md +232 -0
  23. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/gateway/README.md +100 -0
  24. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/gateway/deploy-template.sh +117 -0
  25. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/gateway/deployment-strategies.md +416 -0
  26. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/gateway/troubleshooting-guide.md +384 -0
  27. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/gateway/validate-deployment.sh +186 -0
  28. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/identity/README.md +186 -0
  29. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/memory/README.md +253 -0
  30. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/observability/README.md +517 -0
  31. package/bundles/antigravity-plugin-extras/skills/aws-agentic-ai/services/runtime/README.md +215 -0
  32. package/bundles/antigravity-plugin-extras/skills/aws-cdk-development/SKILL.md +279 -0
  33. package/bundles/antigravity-plugin-extras/skills/aws-cdk-development/references/cdk-patterns.md +431 -0
  34. package/bundles/antigravity-plugin-extras/skills/aws-cdk-development/scripts/validate-stack.sh +240 -0
  35. package/bundles/antigravity-plugin-extras/skills/aws-cost-operations/SKILL.md +318 -0
  36. package/bundles/antigravity-plugin-extras/skills/aws-cost-operations/references/cloudwatch-alarms.md +567 -0
  37. package/bundles/antigravity-plugin-extras/skills/aws-cost-operations/references/operations-patterns.md +394 -0
  38. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/SKILL.md +758 -0
  39. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/references/deployment-best-practices.md +830 -0
  40. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/references/eda-patterns.md +1002 -0
  41. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/references/observability-best-practices.md +770 -0
  42. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/references/performance-optimization.md +671 -0
  43. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/references/security-best-practices.md +625 -0
  44. package/bundles/antigravity-plugin-extras/skills/aws-serverless-eda/references/serverless-patterns.md +838 -0
  45. package/bundles/antigravity-plugin-extras/skills/backend-to-frontend-handoff/SKILL.md +139 -0
  46. package/bundles/antigravity-plugin-extras/skills/banner-design/SKILL.md +188 -0
  47. package/bundles/antigravity-plugin-extras/skills/brand-design/SKILL.md +298 -0
  48. package/bundles/antigravity-plugin-extras/skills/brand-guidelines/LICENSE.txt +202 -0
  49. package/bundles/antigravity-plugin-extras/skills/brand-guidelines/SKILL.md +118 -0
  50. package/bundles/antigravity-plugin-extras/skills/brand-voice/SKILL.md +93 -0
  51. package/bundles/antigravity-plugin-extras/skills/building-ai-agent-on-cloudflare/SKILL.md +392 -0
  52. package/bundles/antigravity-plugin-extras/skills/building-ai-agent-on-cloudflare/references/agent-patterns.md +461 -0
  53. package/bundles/antigravity-plugin-extras/skills/building-ai-agent-on-cloudflare/references/examples.md +188 -0
  54. package/bundles/antigravity-plugin-extras/skills/building-ai-agent-on-cloudflare/references/state-patterns.md +360 -0
  55. package/bundles/antigravity-plugin-extras/skills/building-ai-agent-on-cloudflare/references/troubleshooting.md +362 -0
  56. package/bundles/antigravity-plugin-extras/skills/building-mcp-server-on-cloudflare/SKILL.md +266 -0
  57. package/bundles/antigravity-plugin-extras/skills/building-mcp-server-on-cloudflare/references/examples.md +115 -0
  58. package/bundles/antigravity-plugin-extras/skills/building-mcp-server-on-cloudflare/references/oauth-setup.md +338 -0
  59. package/bundles/antigravity-plugin-extras/skills/building-mcp-server-on-cloudflare/references/troubleshooting.md +317 -0
  60. package/bundles/antigravity-plugin-extras/skills/caching-patterns/SKILL.md +385 -0
  61. package/bundles/antigravity-plugin-extras/skills/canvas-design/LICENSE.txt +202 -0
  62. package/bundles/antigravity-plugin-extras/skills/canvas-design/SKILL.md +91 -0
  63. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  64. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  65. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  66. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
  67. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  68. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
  69. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  70. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  71. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  72. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  73. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  74. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  75. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  76. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  77. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
  78. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  79. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
  80. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  81. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  82. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
  83. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  84. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
  85. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  86. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  87. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  88. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  89. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  90. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  91. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  92. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  93. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  94. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  95. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  96. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  97. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  98. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  99. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  100. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
  101. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  102. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  103. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  104. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  105. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  106. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  107. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
  108. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  109. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  110. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  111. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  112. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  113. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
  114. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  115. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  116. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
  117. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  118. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  119. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  120. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  121. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
  122. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  123. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  124. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
  125. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
  126. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  127. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  128. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
  129. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  130. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
  131. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  132. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  133. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
  134. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  135. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
  136. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  137. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  138. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  139. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  140. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
  141. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  142. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
  143. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  144. package/bundles/antigravity-plugin-extras/skills/changelog-generator/SKILL.md +105 -0
  145. package/bundles/antigravity-plugin-extras/skills/code-explainer/SKILL.md +91 -0
  146. package/bundles/antigravity-plugin-extras/skills/competitive-ads-extractor/SKILL.md +294 -0
  147. package/bundles/antigravity-plugin-extras/skills/component-testing/SKILL.md +310 -0
  148. package/bundles/antigravity-plugin-extras/skills/content-research-writer/SKILL.md +539 -0
  149. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +51 -0
  150. package/bundles/antigravity-plugin-extras/skills/data-analysis/SKILL.md +111 -0
  151. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/README.md +248 -0
  152. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/SKILL.md +203 -0
  153. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/adapter.py.template +44 -0
  154. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/api-router.py.template +43 -0
  155. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/contract-test.py.template +384 -0
  156. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/display-dto.py.template +39 -0
  157. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/frontend-service.ts.template +46 -0
  158. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/frontend-types.ts.template +38 -0
  159. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/ingestion-model.py.template +42 -0
  160. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/mapping-doc.md.template +46 -0
  161. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/normalizer.py.template +48 -0
  162. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/prompts/extract_requirements.md +36 -0
  163. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/prompts/extract_ui_demand.md +35 -0
  164. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/prompts/reconcile_mapping.md +66 -0
  165. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/assets/query-catalog.md.template +62 -0
  166. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/references/consumer-driven-contracts.md +131 -0
  167. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/references/drift-check-catalog.md +305 -0
  168. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/references/layer-responsibilities.md +333 -0
  169. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/discover_connections.py +170 -0
  170. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/discover_sources.py +161 -0
  171. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/drift_check.py +171 -0
  172. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/extract_schema.py +1363 -0
  173. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/generate_gap_report.py +81 -0
  174. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/generate_mapping_doc.py +294 -0
  175. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/ts_dto_compare.py +214 -0
  176. package/bundles/antigravity-plugin-extras/skills/data-contract-pipeline/scripts/validate_lineage.py +51 -0
  177. package/bundles/antigravity-plugin-extras/skills/data-loader/SKILL.md +753 -0
  178. package/bundles/antigravity-plugin-extras/skills/data-validation/SKILL.md +220 -0
  179. package/bundles/antigravity-plugin-extras/skills/db-testing/SKILL.md +117 -0
  180. package/bundles/antigravity-plugin-extras/skills/deploy-local/SKILL.md +259 -0
  181. package/bundles/antigravity-plugin-extras/skills/design-md/SKILL.md +167 -0
  182. package/bundles/antigravity-plugin-extras/skills/design-system-tokens/SKILL.md +240 -0
  183. package/bundles/antigravity-plugin-extras/skills/design-system-tokens/references/DESIGN-SYSTEM-TEMPLATE.md +577 -0
  184. package/bundles/antigravity-plugin-extras/skills/developer-growth-analysis/SKILL.md +323 -0
  185. package/bundles/antigravity-plugin-extras/skills/doc-coauthoring/SKILL.md +378 -0
  186. package/bundles/antigravity-plugin-extras/skills/documentation-analyzer/SKILL.md +78 -0
  187. package/bundles/antigravity-plugin-extras/skills/domain-name-brainstormer/SKILL.md +213 -0
  188. package/bundles/antigravity-plugin-extras/skills/enterprise-dashboard-aesthetic-system/SKILL.md +120 -0
  189. package/bundles/antigravity-plugin-extras/skills/excalidraw/SKILL.md +193 -0
  190. package/bundles/antigravity-plugin-extras/skills/excalidraw/references/element-types.md +497 -0
  191. package/bundles/antigravity-plugin-extras/skills/excalidraw/references/excalidraw-schema.md +356 -0
  192. package/bundles/antigravity-plugin-extras/skills/excalidraw/references/reliable-diagrams.md +115 -0
  193. package/bundles/antigravity-plugin-extras/skills/excalidraw/scripts/README.md +60 -0
  194. package/bundles/antigravity-plugin-extras/skills/excalidraw/scripts/add-arrow.py +354 -0
  195. package/bundles/antigravity-plugin-extras/skills/excalidraw/scripts/add-icon-to-diagram.py +412 -0
  196. package/bundles/antigravity-plugin-extras/skills/excalidraw/scripts/split-excalidraw-library.py +183 -0
  197. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/business-flow-swimlane-template.excalidraw +334 -0
  198. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/class-diagram-template.excalidraw +558 -0
  199. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/data-flow-diagram-template.excalidraw +279 -0
  200. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/er-diagram-template.excalidraw +662 -0
  201. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/flowchart-template.excalidraw +179 -0
  202. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/mindmap-template.excalidraw +244 -0
  203. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/relationship-template.excalidraw +145 -0
  204. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/reliable-flowchart.excalidraw +1 -0
  205. package/bundles/antigravity-plugin-extras/skills/excalidraw/templates/sequence-diagram-template.excalidraw +509 -0
  206. package/bundles/antigravity-plugin-extras/skills/file-organizer/SKILL.md +434 -0
  207. package/bundles/antigravity-plugin-extras/skills/game-changing-features/README.md +221 -0
  208. package/bundles/antigravity-plugin-extras/skills/game-changing-features/SKILL.md +265 -0
  209. package/bundles/antigravity-plugin-extras/skills/github-issues/SKILL.md +359 -0
  210. package/bundles/antigravity-plugin-extras/skills/github-issues/references/templates.md +90 -0
  211. package/bundles/antigravity-plugin-extras/skills/golden-nuggets/SKILL.md +192 -0
  212. package/bundles/antigravity-plugin-extras/skills/humanizer/SKILL.md +194 -0
  213. package/bundles/antigravity-plugin-extras/skills/image-enhancer/SKILL.md +100 -0
  214. package/bundles/antigravity-plugin-extras/skills/intent-writer/SKILL.md +201 -0
  215. package/bundles/antigravity-plugin-extras/skills/internal-comms/LICENSE.txt +202 -0
  216. package/bundles/antigravity-plugin-extras/skills/internal-comms/SKILL.md +124 -0
  217. package/bundles/antigravity-plugin-extras/skills/internal-comms/examples/3p-updates.md +47 -0
  218. package/bundles/antigravity-plugin-extras/skills/internal-comms/examples/company-newsletter.md +65 -0
  219. package/bundles/antigravity-plugin-extras/skills/internal-comms/examples/faq-answers.md +30 -0
  220. package/bundles/antigravity-plugin-extras/skills/internal-comms/examples/general-comms.md +16 -0
  221. package/bundles/antigravity-plugin-extras/skills/invoice-organizer/SKILL.md +447 -0
  222. package/bundles/antigravity-plugin-extras/skills/jira-issues/SKILL.md +182 -0
  223. package/bundles/antigravity-plugin-extras/skills/job-application/SKILL.md +188 -0
  224. package/bundles/antigravity-plugin-extras/skills/lead-research-assistant/SKILL.md +200 -0
  225. package/bundles/antigravity-plugin-extras/skills/llm-application-dev/SKILL.md +217 -0
  226. package/bundles/antigravity-plugin-extras/skills/mcp-builder/LICENSE.txt +202 -0
  227. package/bundles/antigravity-plugin-extras/skills/mcp-builder/SKILL.md +100 -0
  228. package/bundles/antigravity-plugin-extras/skills/mcp-builder/references/evaluation.md +602 -0
  229. package/bundles/antigravity-plugin-extras/skills/mcp-builder/references/mcp_best_practices.md +915 -0
  230. package/bundles/antigravity-plugin-extras/skills/mcp-builder/references/node_mcp_server.md +916 -0
  231. package/bundles/antigravity-plugin-extras/skills/mcp-builder/references/python_mcp_server.md +752 -0
  232. package/bundles/antigravity-plugin-extras/skills/mcp-builder/scripts/connections.py +151 -0
  233. package/bundles/antigravity-plugin-extras/skills/mcp-builder/scripts/evaluation.py +373 -0
  234. package/bundles/antigravity-plugin-extras/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  235. package/bundles/antigravity-plugin-extras/skills/mcp-builder/scripts/requirements.txt +2 -0
  236. package/bundles/antigravity-plugin-extras/skills/meeting-insights-analyzer/SKILL.md +328 -0
  237. package/bundles/antigravity-plugin-extras/skills/monorepo/SKILL.md +267 -0
  238. package/bundles/antigravity-plugin-extras/skills/naming-analyzer/SKILL.md +376 -0
  239. package/bundles/antigravity-plugin-extras/skills/nextjs-app-router/SKILL.md +271 -0
  240. package/bundles/antigravity-plugin-extras/skills/notebooklm/AUTHENTICATION.md +154 -0
  241. package/bundles/antigravity-plugin-extras/skills/notebooklm/SKILL.md +270 -0
  242. package/bundles/antigravity-plugin-extras/skills/notebooklm/references/api_reference.md +309 -0
  243. package/bundles/antigravity-plugin-extras/skills/notebooklm/references/troubleshooting.md +376 -0
  244. package/bundles/antigravity-plugin-extras/skills/notebooklm/references/usage_patterns.md +338 -0
  245. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/__init__.py +81 -0
  246. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/ask_question.py +256 -0
  247. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/auth_manager.py +358 -0
  248. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/browser_session.py +255 -0
  249. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/browser_utils.py +107 -0
  250. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/cleanup_manager.py +302 -0
  251. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/config.py +44 -0
  252. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/notebook_manager.py +410 -0
  253. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/run.py +102 -0
  254. package/bundles/antigravity-plugin-extras/skills/notebooklm/scripts/setup_environment.py +204 -0
  255. package/bundles/antigravity-plugin-extras/skills/observability/SKILL.md +371 -0
  256. package/bundles/antigravity-plugin-extras/skills/onboard-project/SKILL.md +192 -0
  257. package/bundles/antigravity-plugin-extras/skills/performance-testing/SKILL.md +226 -0
  258. package/bundles/antigravity-plugin-extras/skills/pipeline-state/SKILL.md +208 -0
  259. package/bundles/antigravity-plugin-extras/skills/plantuml/SKILL.md +228 -0
  260. package/bundles/antigravity-plugin-extras/skills/popular-web-designs/SKILL.md +142 -0
  261. package/bundles/antigravity-plugin-extras/skills/popular-web-designs/templates/linear.app.md +196 -0
  262. package/bundles/antigravity-plugin-extras/skills/popular-web-designs/templates/stripe.md +214 -0
  263. package/bundles/antigravity-plugin-extras/skills/popular-web-designs/templates/vercel.md +218 -0
  264. package/bundles/antigravity-plugin-extras/skills/prd-to-code/SKILL.md +127 -0
  265. package/bundles/antigravity-plugin-extras/skills/raffle-winner-picker/SKILL.md +159 -0
  266. package/bundles/antigravity-plugin-extras/skills/react-useeffect/README.md +320 -0
  267. package/bundles/antigravity-plugin-extras/skills/react-useeffect/SKILL.md +53 -0
  268. package/bundles/antigravity-plugin-extras/skills/react-useeffect/alternatives.md +258 -0
  269. package/bundles/antigravity-plugin-extras/skills/react-useeffect/anti-patterns.md +290 -0
  270. package/bundles/antigravity-plugin-extras/skills/receiving-code-review/SKILL.md +213 -0
  271. package/bundles/antigravity-plugin-extras/skills/relay/SKILL.md +246 -0
  272. package/bundles/antigravity-plugin-extras/skills/requesting-code-review/SKILL.md +105 -0
  273. package/bundles/antigravity-plugin-extras/skills/requesting-code-review/code-reviewer.md +146 -0
  274. package/bundles/antigravity-plugin-extras/skills/responsive-mobile-native/SKILL.md +350 -0
  275. package/bundles/antigravity-plugin-extras/skills/responsive-mobile-native/references/pwa-checklist.md +226 -0
  276. package/bundles/antigravity-plugin-extras/skills/responsive-mobile-native/references/touch-and-interaction.md +265 -0
  277. package/bundles/antigravity-plugin-extras/skills/responsive-mobile-native/references/transformation-catalogue.md +326 -0
  278. package/bundles/antigravity-plugin-extras/skills/schema-migration/SKILL.md +275 -0
  279. package/bundles/antigravity-plugin-extras/skills/setup-project-ai/SKILL.md +120 -0
  280. package/bundles/antigravity-plugin-extras/skills/shadcn-radix/SKILL.md +330 -0
  281. package/bundles/antigravity-plugin-extras/skills/sketch/SKILL.md +104 -0
  282. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/LICENSE.txt +202 -0
  283. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/SKILL.md +646 -0
  284. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/color_palettes.py +302 -0
  285. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/easing.py +230 -0
  286. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/frame_composer.py +469 -0
  287. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/gif_builder.py +246 -0
  288. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/typography.py +357 -0
  289. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/validators.py +264 -0
  290. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/core/visual_effects.py +494 -0
  291. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/requirements.txt +4 -0
  292. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/bounce.py +106 -0
  293. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/explode.py +331 -0
  294. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/fade.py +329 -0
  295. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/flip.py +291 -0
  296. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  297. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/morph.py +329 -0
  298. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/move.py +293 -0
  299. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/pulse.py +268 -0
  300. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/shake.py +127 -0
  301. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/slide.py +291 -0
  302. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/spin.py +269 -0
  303. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/wiggle.py +300 -0
  304. package/bundles/antigravity-plugin-extras/skills/slack-gif-creator/templates/zoom.py +312 -0
  305. package/bundles/antigravity-plugin-extras/skills/slides/SKILL.md +38 -0
  306. package/bundles/antigravity-plugin-extras/skills/state-tracking/SKILL.md +131 -0
  307. package/bundles/antigravity-plugin-extras/skills/streamlit-dev/SKILL.md +268 -0
  308. package/bundles/antigravity-plugin-extras/skills/svg-create/SKILL.md +251 -0
  309. package/bundles/antigravity-plugin-extras/skills/theme-factory/LICENSE.txt +202 -0
  310. package/bundles/antigravity-plugin-extras/skills/theme-factory/SKILL.md +59 -0
  311. package/bundles/antigravity-plugin-extras/skills/theme-factory/theme-showcase.pdf +0 -0
  312. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/arctic-frost.md +19 -0
  313. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/botanical-garden.md +19 -0
  314. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/desert-rose.md +19 -0
  315. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/forest-canopy.md +19 -0
  316. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/golden-hour.md +19 -0
  317. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  318. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/modern-minimalist.md +19 -0
  319. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/ocean-depths.md +19 -0
  320. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  321. package/bundles/antigravity-plugin-extras/skills/theme-factory/themes/tech-innovation.md +19 -0
  322. package/bundles/antigravity-plugin-extras/skills/ui-styling-patterns/LICENSE.txt +202 -0
  323. package/bundles/antigravity-plugin-extras/skills/ui-styling-patterns/SKILL.md +320 -0
  324. package/bundles/antigravity-plugin-extras/skills/ui-testing/SKILL.md +660 -0
  325. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/SKILL.md +660 -0
  326. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/_sync_all.py +414 -0
  327. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/app-interface.csv +31 -0
  328. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/charts.csv +26 -0
  329. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/colors.csv +162 -0
  330. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/design.csv +1776 -0
  331. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/draft.csv +1779 -0
  332. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/google-fonts.csv +1924 -0
  333. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/icons.csv +106 -0
  334. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/landing.csv +35 -0
  335. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/products.csv +162 -0
  336. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/react-performance.csv +45 -0
  337. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/styles.csv +85 -0
  338. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/typography.csv +74 -0
  339. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/ui-reasoning.csv +162 -0
  340. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/data/ux-guidelines.csv +100 -0
  341. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/scripts/core.py +247 -0
  342. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/scripts/design_system.py +1067 -0
  343. package/bundles/antigravity-plugin-extras/skills/ui-ux-intelligence/scripts/search.py +114 -0
  344. package/bundles/antigravity-plugin-extras/skills/understand-anything/SKILL.md +140 -0
  345. package/bundles/antigravity-plugin-extras/skills/understand-anything/references/commands.md +252 -0
  346. package/bundles/antigravity-plugin-extras/skills/understand-anything/references/graph-schema.md +116 -0
  347. package/bundles/antigravity-plugin-extras/skills/understand-anything/references/pipeline.md +331 -0
  348. package/bundles/antigravity-plugin-extras/skills/update-readme/SKILL.md +74 -0
  349. package/bundles/antigravity-plugin-extras/skills/verification-loop/SKILL.md +209 -0
  350. package/bundles/antigravity-plugin-extras/skills/video-downloader/SKILL.md +107 -0
  351. package/bundles/antigravity-plugin-extras/skills/webapp-development/SKILL.md +288 -0
  352. package/bundles/antigravity-plugin-extras/skills/word-cloud/SKILL.md +339 -0
  353. package/bundles/antigravity-plugin-extras/skills/word-cloud/agents/agent-interface.md +175 -0
  354. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/html.md +384 -0
  355. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/nlp-keywords.md +127 -0
  356. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/palettes.md +149 -0
  357. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/python.md +249 -0
  358. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/react.md +427 -0
  359. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/shapes.md +135 -0
  360. package/bundles/antigravity-plugin-extras/skills/word-cloud/references/stopwords.md +115 -0
  361. package/bundles/antigravity-plugin-extras/skills/youtube-transcript/SKILL.md +416 -0
  362. package/bundles/manifest.json +2 -1
  363. package/dist/cli.js +166 -37
  364. package/package.json +1 -1
@@ -0,0 +1,1363 @@
1
+ """
2
+ Unified Schema Extractor — Reads any structured data source and generates Pydantic models.
3
+
4
+ Supported formats:
5
+ - JSON (.json) — recursive nesting, list-of-objects, Field(alias=...)
6
+ - Markdown (.md) — tables, bold KV, code blocks, bullet KV, outer wrapper strip
7
+ - CSV/TSV (.csv/.tsv) — column headers, type inference from sample rows
8
+ - XLSX (.xlsx/.xls) — openpyxl, same column logic as CSV
9
+ - YAML (.yaml/.yml) — safe_load, delegates to dict logic
10
+ - Plain text (.txt/.log) — Key: Value, Key = Value, [Section] headers
11
+ - DB table (--format db) — SQLAlchemy inspect + data sampling + embedded format detection
12
+
13
+ DB discovery & sampling:
14
+ --discover — enumerate all tables/views with columns, PKs, FKs
15
+ --schema <name> — target a specific DB schema (e.g., 'dbo', 'public')
16
+ --sample N — fetch N rows for type inference and embedded format detection
17
+ Embedded formats detected: JSON, XML, YAML, markdown, pipe-delimited in string columns
18
+
19
+ Schema evolution:
20
+ --save-baseline snap.json — snapshot current schema for later comparison
21
+ --diff snap.json — compare current extraction vs baseline
22
+
23
+ Usage:
24
+ python extract_schema.py data.json
25
+ python extract_schema.py report.md --class-name NetworkReport
26
+ python extract_schema.py data.csv --output models/ingestion/report.py
27
+ python extract_schema.py data.json --save-baseline baseline.json
28
+ python extract_schema.py data_v2.json --diff baseline.json
29
+ python extract_schema.py --format db --connection-string "mssql+pyodbc://..." --discover
30
+ python extract_schema.py --format db --connection-string "..." --table users --sample 50
31
+ python extract_schema.py --format db --connection-string "..." --table orders --schema dbo
32
+
33
+ Part of the data-contract-pipeline skill.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import csv
39
+ import io
40
+ import json
41
+ import re
42
+ import sys
43
+ from dataclasses import dataclass, field
44
+ from datetime import datetime, timezone
45
+ from pathlib import Path
46
+ from typing import Any
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Data structures
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ @dataclass
54
+ class ExtractedField:
55
+ name: str
56
+ python_type: str
57
+ source_pattern: str
58
+ example_value: str = ""
59
+ section: str = "Root"
60
+
61
+
62
+ @dataclass
63
+ class ExtractedSection:
64
+ name: str
65
+ fields: list[ExtractedField] = field(default_factory=list)
66
+ table_rows_model: str | None = None
67
+
68
+
69
+ @dataclass
70
+ class SchemaChange:
71
+ field_name: str
72
+ change_type: str # added, removed, type_changed
73
+ old_value: str = ""
74
+ new_value: str = ""
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Shared helpers
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ def to_snake_case(name: str) -> str:
83
+ name = name.replace("**", "").strip().rstrip(":")
84
+ s = re.sub(r"[^a-zA-Z0-9]", "_", name)
85
+ s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", s)
86
+ s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
87
+ s = re.sub(r"_+", "_", s).strip("_")
88
+ return s.lower()
89
+
90
+
91
+ def to_class_name(name: str) -> str:
92
+ snake = to_snake_case(name)
93
+ return "".join(word.capitalize() for word in snake.split("_"))
94
+
95
+
96
+ def infer_type_from_value(value: Any) -> str:
97
+ if value is None:
98
+ return "Any | None"
99
+ if isinstance(value, bool):
100
+ return "bool"
101
+ if isinstance(value, int):
102
+ return "int"
103
+ if isinstance(value, float):
104
+ return "float"
105
+ if isinstance(value, list):
106
+ if not value:
107
+ return "list"
108
+ types = {infer_type_from_value(v) for v in value}
109
+ return f"list[{types.pop()}]" if len(types) == 1 else "list"
110
+ if isinstance(value, dict):
111
+ return "dict"
112
+ v = str(value).strip()
113
+ if not v or v.lower() in ("n/a", "not available", "none", "null", "-"):
114
+ return "str | None"
115
+ if v.lower() in ("true", "false", "yes", "no"):
116
+ return "bool"
117
+ try:
118
+ int(v.replace(",", ""))
119
+ return "int"
120
+ except ValueError:
121
+ pass
122
+ try:
123
+ float(v.replace(",", ""))
124
+ return "float"
125
+ except ValueError:
126
+ pass
127
+ return "str"
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Format detection
132
+ # ---------------------------------------------------------------------------
133
+
134
+ FORMAT_MAP = {
135
+ ".json": "json",
136
+ ".md": "md",
137
+ ".markdown": "md",
138
+ ".csv": "csv",
139
+ ".tsv": "csv",
140
+ ".xlsx": "xlsx",
141
+ ".xls": "xlsx",
142
+ ".yaml": "yaml",
143
+ ".yml": "yaml",
144
+ ".txt": "text",
145
+ ".log": "text",
146
+ }
147
+
148
+
149
+ def detect_format(path: Path | None, explicit: str | None) -> str:
150
+ if explicit and explicit != "auto":
151
+ return explicit
152
+ if path:
153
+ return FORMAT_MAP.get(path.suffix.lower(), "text")
154
+ return "text"
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # JSON extractor
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ def extract_json_fields(
163
+ data: Any, section: str = "Root"
164
+ ) -> list[ExtractedSection]:
165
+ sections: list[ExtractedSection] = []
166
+ if isinstance(data, list):
167
+ data = data[0] if data else {}
168
+ if not isinstance(data, dict):
169
+ return sections
170
+
171
+ root = ExtractedSection(name=section)
172
+ for key, value in data.items():
173
+ snake = to_snake_case(key)
174
+ if not snake or not snake.replace("_", "").isalnum():
175
+ continue
176
+
177
+ if isinstance(value, dict) and value:
178
+ child_class = to_class_name(key)
179
+ child_sections = extract_json_fields(value, section=key)
180
+ sections.extend(child_sections)
181
+ root.fields.append(
182
+ ExtractedField(
183
+ name=snake,
184
+ python_type=child_class,
185
+ source_pattern="json_nested",
186
+ example_value=f"{len(value)} keys",
187
+ section=section,
188
+ )
189
+ )
190
+ elif isinstance(value, list) and value and isinstance(value[0], dict):
191
+ row_class = to_class_name(key) + "Item"
192
+ row_sections = extract_json_fields(value[0], section=f"{key} (Item)")
193
+ for rs in row_sections:
194
+ if rs.name == f"{key} (Item)":
195
+ rs.name = f"{key} (Row Model)"
196
+ sections.extend(row_sections)
197
+ root.fields.append(
198
+ ExtractedField(
199
+ name=snake,
200
+ python_type=f"list[{row_class}]",
201
+ source_pattern="json_list",
202
+ example_value=f"{len(value)} items",
203
+ section=section,
204
+ )
205
+ )
206
+ else:
207
+ example = str(value)[:80] if value is not None else "null"
208
+ root.fields.append(
209
+ ExtractedField(
210
+ name=snake,
211
+ python_type=infer_type_from_value(value),
212
+ source_pattern="json_key",
213
+ example_value=example,
214
+ section=section,
215
+ )
216
+ )
217
+
218
+ if root.fields:
219
+ sections.insert(0, root)
220
+ return sections
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Markdown extractor
225
+ # ---------------------------------------------------------------------------
226
+
227
+
228
+ def strip_outer_table_wrapper(text: str) -> str:
229
+ """Strip single-cell table wrapper (| col | / | --- | header) from LLM outputs."""
230
+ lines = text.strip().split("\n")
231
+ if len(lines) < 3:
232
+ return text
233
+ header = lines[0].strip()
234
+ if not re.match(r"^\|[^|]+\|\s*$", header):
235
+ return text
236
+ separator = lines[1].strip()
237
+ if not re.match(r"^\|[-\s:]+\|\s*$", separator):
238
+ return text
239
+ content_lines = lines[2:]
240
+ if content_lines:
241
+ first = content_lines[0].strip()
242
+ if first.startswith("|"):
243
+ content_lines[0] = first[1:].lstrip()
244
+ return "\n".join(content_lines)
245
+
246
+
247
+ def parse_markdown_tables(text: str) -> list[dict]:
248
+ tables: list[dict] = []
249
+ lines = text.split("\n")
250
+ i = 0
251
+ while i < len(lines) - 1:
252
+ line = lines[i].strip()
253
+ # Separator regex: dash MUST be first in char class to avoid range
254
+ if re.match(r"^\|[-\s:]+\|[-\s:|]+\|$", line.replace(" ", "")):
255
+ if i > 0 and "|" in lines[i - 1]:
256
+ header_row = lines[i - 1].strip()
257
+ data_rows: list[str] = []
258
+ j = i + 1
259
+ while j < len(lines) and "|" in lines[j] and lines[j].strip():
260
+ data_rows.append(lines[j].strip())
261
+ j += 1
262
+ header_cells = [
263
+ c.strip()
264
+ for c in header_row.strip().strip("|").split("|")
265
+ if c.strip()
266
+ ]
267
+ tables.append(
268
+ {
269
+ "header_row": header_row,
270
+ "data_rows": data_rows,
271
+ "column_count": len(header_cells),
272
+ }
273
+ )
274
+ i = j
275
+ continue
276
+ i += 1
277
+ return tables
278
+
279
+
280
+ def extract_bold_kv(text: str) -> list[ExtractedField]:
281
+ fields: list[ExtractedField] = []
282
+ for m in re.finditer(
283
+ r"\*\*(.+?)\*\*\s*:?\s*(.+?)(?:\s{2,}|$)", text, re.MULTILINE
284
+ ):
285
+ key = m.group(1).strip().rstrip(":")
286
+ value = m.group(2).strip()
287
+ if value.startswith("**") or value.startswith("#"):
288
+ continue
289
+ snake = to_snake_case(key)
290
+ if not snake or not snake.replace("_", "").isalnum():
291
+ continue
292
+ fields.append(
293
+ ExtractedField(
294
+ name=snake,
295
+ python_type=infer_type_from_value(value),
296
+ source_pattern="bold_kv",
297
+ example_value=value[:80],
298
+ )
299
+ )
300
+ return fields
301
+
302
+
303
+ def extract_code_block_kv(block: str) -> list[ExtractedField]:
304
+ fields: list[ExtractedField] = []
305
+ seen: set[str] = set()
306
+ for line in block.split("\n"):
307
+ line = line.strip()
308
+ if not line or line.startswith("\u2501") or line.startswith("---"):
309
+ continue
310
+ for seg in line.split("|"):
311
+ seg = seg.strip()
312
+ m = re.match(r"^([A-Za-z][A-Za-z0-9 _/#()-]+?)\s*:\s*(.+)$", seg)
313
+ if m:
314
+ snake = to_snake_case(m.group(1).strip())
315
+ if (
316
+ snake
317
+ and snake.replace("_", "").isalnum()
318
+ and snake not in seen
319
+ ):
320
+ seen.add(snake)
321
+ fields.append(
322
+ ExtractedField(
323
+ name=snake,
324
+ python_type=infer_type_from_value(
325
+ m.group(2).strip()
326
+ ),
327
+ source_pattern="code_block_kv",
328
+ example_value=m.group(2).strip()[:80],
329
+ )
330
+ )
331
+ return fields
332
+
333
+
334
+ def extract_bullet_kv(
335
+ text: str, already: set[str] | None = None
336
+ ) -> list[ExtractedField]:
337
+ """Extract non-bold bullet KV pairs. Bold bullets are handled by bold_kv."""
338
+ if already is None:
339
+ already = set()
340
+ fields: list[ExtractedField] = []
341
+ seen: set[str] = set()
342
+ for m in re.finditer(
343
+ r"^[-*]\s+(?!\*\*)([^*:]+?)\s*:\s*(.+)$", text, re.MULTILINE
344
+ ):
345
+ snake = to_snake_case(m.group(1).strip())
346
+ if (
347
+ snake
348
+ and snake.replace("_", "").isalnum()
349
+ and snake not in seen
350
+ and snake not in already
351
+ ):
352
+ seen.add(snake)
353
+ fields.append(
354
+ ExtractedField(
355
+ name=snake,
356
+ python_type=infer_type_from_value(m.group(2).strip()),
357
+ source_pattern="bullet_kv",
358
+ example_value=m.group(2).strip()[:80],
359
+ )
360
+ )
361
+ return fields
362
+
363
+
364
+ def extract_md_sections(text: str) -> list[tuple[str, str]]:
365
+ sections: list[tuple[str, str]] = []
366
+ matches = list(re.finditer(r"^#{1,3}\s+(.+)$", text, re.MULTILINE))
367
+ if not matches:
368
+ return [("Root", text)]
369
+ if matches[0].start() > 0:
370
+ sections.append(("Root", text[: matches[0].start()]))
371
+ for i, m in enumerate(matches):
372
+ heading = re.sub(r"^\d+\.\s*", "", m.group(1).strip())
373
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
374
+ sections.append((heading, text[m.end() : end].strip()))
375
+ return sections
376
+
377
+
378
+ def extract_markdown(text: str) -> list[ExtractedSection]:
379
+ text = strip_outer_table_wrapper(text)
380
+ result: list[ExtractedSection] = []
381
+ for heading, content in extract_md_sections(text):
382
+ section = ExtractedSection(name=heading)
383
+
384
+ # Markdown tables
385
+ for table in parse_markdown_tables(content):
386
+ if table["column_count"] == 2:
387
+ for row in table["data_rows"]:
388
+ cells = [
389
+ c.strip().replace("**", "")
390
+ for c in row.strip().strip("|").split("|")
391
+ ]
392
+ if len(cells) >= 2:
393
+ snake = to_snake_case(cells[0])
394
+ if snake and snake.replace("_", "").isalnum():
395
+ section.fields.append(
396
+ ExtractedField(
397
+ name=snake,
398
+ python_type=infer_type_from_value(
399
+ cells[1].strip()
400
+ ),
401
+ source_pattern="md_table_kv",
402
+ example_value=cells[1].strip()[:80],
403
+ )
404
+ )
405
+ elif table["column_count"] >= 3:
406
+ headers = [
407
+ c.strip().replace("**", "")
408
+ for c in table["header_row"].strip().strip("|").split("|")
409
+ if c.strip()
410
+ ]
411
+ first_vals = (
412
+ [
413
+ c.strip().replace("**", "")
414
+ for c in table["data_rows"][0]
415
+ .strip()
416
+ .strip("|")
417
+ .split("|")
418
+ if c.strip()
419
+ ]
420
+ if table["data_rows"]
421
+ else []
422
+ )
423
+ row_fields = []
424
+ for idx, h in enumerate(headers):
425
+ snake = to_snake_case(h)
426
+ if snake and snake.replace("_", "").isalnum():
427
+ val = first_vals[idx] if idx < len(first_vals) else ""
428
+ row_fields.append(
429
+ ExtractedField(
430
+ name=snake,
431
+ python_type=infer_type_from_value(val),
432
+ source_pattern="md_table_multi",
433
+ example_value=val[:80],
434
+ )
435
+ )
436
+ row_class = to_class_name(heading) + "Row"
437
+ result.append(
438
+ ExtractedSection(
439
+ name=f"{heading} (Row Model)", fields=row_fields
440
+ )
441
+ )
442
+ section.fields.append(
443
+ ExtractedField(
444
+ name=to_snake_case(heading) + "_rows",
445
+ python_type=f"list[{row_class}]",
446
+ source_pattern="md_table_multi",
447
+ example_value=f"{len(table['data_rows'])} rows",
448
+ )
449
+ )
450
+
451
+ # Code blocks
452
+ for block in re.findall(r"```[^\n]*\n(.*?)```", content, re.DOTALL):
453
+ section.fields.extend(extract_code_block_kv(block))
454
+
455
+ # Bold + bullet KV (after removing tables and code blocks)
456
+ clean = re.sub(r"```.*?```", "", content, flags=re.DOTALL)
457
+ clean = re.sub(r"^\|.+\|$", "", clean, flags=re.MULTILINE)
458
+ bold_fields = extract_bold_kv(clean)
459
+ section.fields.extend(bold_fields)
460
+ bold_names = {f.name for f in bold_fields}
461
+ section.fields.extend(extract_bullet_kv(clean, already=bold_names))
462
+
463
+ if section.fields:
464
+ if heading == "Root":
465
+ result.insert(0, section)
466
+ else:
467
+ result.append(section)
468
+ return result
469
+
470
+
471
+ # ---------------------------------------------------------------------------
472
+ # CSV extractor
473
+ # ---------------------------------------------------------------------------
474
+
475
+
476
+ def extract_csv(
477
+ text: str, delimiter: str | None = None
478
+ ) -> list[ExtractedSection]:
479
+ if delimiter is None:
480
+ try:
481
+ dialect = csv.Sniffer().sniff(text[:4096])
482
+ delimiter = dialect.delimiter
483
+ except csv.Error:
484
+ delimiter = ","
485
+
486
+ reader = csv.reader(io.StringIO(text), delimiter=delimiter)
487
+ rows = list(reader)
488
+ if len(rows) < 2:
489
+ return []
490
+
491
+ headers = rows[0]
492
+ data_rows = rows[1:101] # sample up to 100 rows for type inference
493
+
494
+ section = ExtractedSection(name="Root")
495
+ for col_idx, header in enumerate(headers):
496
+ snake = to_snake_case(header)
497
+ if not snake or not snake.replace("_", "").isalnum():
498
+ snake = f"col_{col_idx}"
499
+
500
+ sample_values = [
501
+ r[col_idx]
502
+ for r in data_rows
503
+ if col_idx < len(r) and r[col_idx].strip()
504
+ ]
505
+ if sample_values:
506
+ types = {infer_type_from_value(v) for v in sample_values[:20]}
507
+ if len(types) == 1:
508
+ py_type = types.pop()
509
+ elif types <= {"int", "float"}:
510
+ py_type = "float"
511
+ elif "str | None" in types:
512
+ py_type = "str | None"
513
+ else:
514
+ py_type = "str"
515
+ example = sample_values[0][:80]
516
+ else:
517
+ py_type = "str | None"
518
+ example = ""
519
+
520
+ section.fields.append(
521
+ ExtractedField(
522
+ name=snake,
523
+ python_type=py_type,
524
+ source_pattern="csv_col",
525
+ example_value=example,
526
+ )
527
+ )
528
+ return [section]
529
+
530
+
531
+ # ---------------------------------------------------------------------------
532
+ # XLSX extractor
533
+ # ---------------------------------------------------------------------------
534
+
535
+
536
+ def extract_xlsx(path: Path) -> list[ExtractedSection]:
537
+ try:
538
+ import openpyxl
539
+ except ImportError:
540
+ print(
541
+ "Error: openpyxl required for xlsx. Install: pip install openpyxl",
542
+ file=sys.stderr,
543
+ )
544
+ sys.exit(1)
545
+
546
+ wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
547
+ all_sections: list[ExtractedSection] = []
548
+
549
+ for sheet_name in wb.sheetnames:
550
+ ws = wb[sheet_name]
551
+ rows = list(ws.iter_rows(values_only=True))
552
+ if len(rows) < 2:
553
+ continue
554
+
555
+ headers = [
556
+ str(c) if c else f"col_{i}" for i, c in enumerate(rows[0])
557
+ ]
558
+ data_rows = rows[1:101]
559
+
560
+ section = ExtractedSection(
561
+ name=sheet_name if len(wb.sheetnames) > 1 else "Root"
562
+ )
563
+ for col_idx, header in enumerate(headers):
564
+ snake = to_snake_case(header)
565
+ if not snake or not snake.replace("_", "").isalnum():
566
+ snake = f"col_{col_idx}"
567
+
568
+ samples = [
569
+ r[col_idx]
570
+ for r in data_rows
571
+ if col_idx < len(r) and r[col_idx] is not None
572
+ ]
573
+ if samples:
574
+ py_type = infer_type_from_value(samples[0])
575
+ example = str(samples[0])[:80]
576
+ else:
577
+ py_type = "str | None"
578
+ example = ""
579
+
580
+ section.fields.append(
581
+ ExtractedField(
582
+ name=snake,
583
+ python_type=py_type,
584
+ source_pattern="xlsx_col",
585
+ example_value=example,
586
+ )
587
+ )
588
+ all_sections.append(section)
589
+
590
+ wb.close()
591
+ return all_sections
592
+
593
+
594
+ # ---------------------------------------------------------------------------
595
+ # YAML extractor
596
+ # ---------------------------------------------------------------------------
597
+
598
+
599
+ def extract_yaml(text: str) -> list[ExtractedSection]:
600
+ try:
601
+ import yaml
602
+ except ImportError:
603
+ print(
604
+ "Error: PyYAML required. Install: pip install pyyaml",
605
+ file=sys.stderr,
606
+ )
607
+ sys.exit(1)
608
+ data = yaml.safe_load(text)
609
+ return extract_json_fields(data)
610
+
611
+
612
+ # ---------------------------------------------------------------------------
613
+ # Plain text extractor
614
+ # ---------------------------------------------------------------------------
615
+
616
+
617
+ def extract_text(text: str) -> list[ExtractedSection]:
618
+ sections: list[ExtractedSection] = []
619
+ current_section = "Root"
620
+ current_fields: list[ExtractedField] = []
621
+
622
+ for line in text.split("\n"):
623
+ line = line.strip()
624
+ if not line:
625
+ continue
626
+
627
+ # INI-style section header
628
+ sec_match = re.match(r"^\[(.+)\]$", line)
629
+ if sec_match:
630
+ if current_fields:
631
+ sections.append(
632
+ ExtractedSection(
633
+ name=current_section, fields=current_fields
634
+ )
635
+ )
636
+ current_section = sec_match.group(1).strip()
637
+ current_fields = []
638
+ continue
639
+
640
+ # Key: Value or Key = Value
641
+ kv_match = re.match(
642
+ r"^([A-Za-z][A-Za-z0-9 _.-]+?)\s*[:=]\s*(.+)$", line
643
+ )
644
+ if kv_match:
645
+ snake = to_snake_case(kv_match.group(1).strip())
646
+ value = kv_match.group(2).strip()
647
+ if snake and snake.replace("_", "").isalnum():
648
+ current_fields.append(
649
+ ExtractedField(
650
+ name=snake,
651
+ python_type=infer_type_from_value(value),
652
+ source_pattern="text_kv",
653
+ example_value=value[:80],
654
+ )
655
+ )
656
+
657
+ if current_fields:
658
+ sections.append(
659
+ ExtractedSection(name=current_section, fields=current_fields)
660
+ )
661
+ return sections
662
+
663
+
664
+ # ---------------------------------------------------------------------------
665
+ # DB table extractor
666
+ # ---------------------------------------------------------------------------
667
+
668
+ SQL_TYPE_MAP = {
669
+ # SQL Server
670
+ "INTEGER": "int",
671
+ "BIGINT": "int",
672
+ "SMALLINT": "int",
673
+ "TINYINT": "int",
674
+ "FLOAT": "float",
675
+ "REAL": "float",
676
+ "DECIMAL": "float",
677
+ "NUMERIC": "float",
678
+ "MONEY": "float",
679
+ "SMALLMONEY": "float",
680
+ "BIT": "bool",
681
+ "VARCHAR": "str",
682
+ "NVARCHAR": "str",
683
+ "CHAR": "str",
684
+ "NCHAR": "str",
685
+ "TEXT": "str",
686
+ "NTEXT": "str",
687
+ "DATE": "str",
688
+ "DATETIME": "str",
689
+ "DATETIME2": "str",
690
+ "SMALLDATETIME": "str",
691
+ "TIME": "str",
692
+ "DATETIMEOFFSET": "str",
693
+ "UNIQUEIDENTIFIER": "str",
694
+ "VARBINARY": "bytes",
695
+ "BINARY": "bytes",
696
+ "IMAGE": "bytes",
697
+ # PostgreSQL
698
+ "SERIAL": "int",
699
+ "BIGSERIAL": "int",
700
+ "SMALLSERIAL": "int",
701
+ "BOOLEAN": "bool",
702
+ "DOUBLE_PRECISION": "float",
703
+ "JSONB": "dict",
704
+ "JSON": "dict",
705
+ "UUID": "str",
706
+ "BYTEA": "bytes",
707
+ "TIMESTAMPTZ": "str",
708
+ "TIMESTAMP": "str",
709
+ "INTERVAL": "str",
710
+ "CITEXT": "str",
711
+ "INET": "str",
712
+ "MACADDR": "str",
713
+ "ARRAY": "list",
714
+ "HSTORE": "dict",
715
+ # MySQL
716
+ "TINYTEXT": "str",
717
+ "MEDIUMTEXT": "str",
718
+ "LONGTEXT": "str",
719
+ "MEDIUMINT": "int",
720
+ "ENUM": "str",
721
+ "SET": "str",
722
+ "BLOB": "bytes",
723
+ "MEDIUMBLOB": "bytes",
724
+ "LONGBLOB": "bytes",
725
+ "TINYBLOB": "bytes",
726
+ # SQLite
727
+ "REAL": "float",
728
+ }
729
+
730
+
731
+ def _detect_embedded_format(value: str) -> str | None:
732
+ """Detect structured data embedded in a string column value."""
733
+ v = value.strip()
734
+ if not v:
735
+ return None
736
+ if (v.startswith("{") and v.endswith("}")) or (
737
+ v.startswith("[") and v.endswith("]")
738
+ ):
739
+ try:
740
+ json.loads(v)
741
+ return "json"
742
+ except (json.JSONDecodeError, ValueError):
743
+ pass
744
+ if v.startswith("<?xml") or (v.startswith("<") and ">" in v):
745
+ return "xml"
746
+ if v.startswith("---\n") or re.match(r"^[a-zA-Z_]+\s*:", v):
747
+ try:
748
+ import yaml
749
+
750
+ yaml.safe_load(v)
751
+ return "yaml"
752
+ except Exception:
753
+ pass
754
+ if "|" in v and v.count("|") >= 3:
755
+ return "pipe-delimited"
756
+ if v.startswith("#") or v.startswith("**") or re.search(r"\[.+\]\(.+\)", v):
757
+ return "markdown"
758
+ return None
759
+
760
+
761
+ def _sample_rows(
762
+ engine: Any, table_name: str, schema: str | None, limit: int = 50
763
+ ) -> list[dict]:
764
+ """Fetch sample rows from a table for type inference and format detection."""
765
+ from sqlalchemy import text
766
+
767
+ qualified = f'"{schema}"."{table_name}"' if schema else f'"{table_name}"'
768
+ query = text(f"SELECT * FROM {qualified} ORDER BY 1 LIMIT :lim")
769
+ try:
770
+ with engine.connect() as conn:
771
+ result = conn.execute(query, {"lim": limit})
772
+ columns = list(result.keys())
773
+ return [dict(zip(columns, row)) for row in result.fetchall()]
774
+ except Exception:
775
+ # MSSQL uses TOP instead of LIMIT
776
+ query_mssql = text(
777
+ f"SELECT TOP(:lim) * FROM {qualified} ORDER BY 1"
778
+ )
779
+ try:
780
+ with engine.connect() as conn:
781
+ result = conn.execute(query_mssql, {"lim": limit})
782
+ columns = list(result.keys())
783
+ return [dict(zip(columns, row)) for row in result.fetchall()]
784
+ except Exception:
785
+ return []
786
+
787
+
788
+ def discover_db_tables(
789
+ connection_string: str, schema: str | None = None
790
+ ) -> list[dict]:
791
+ """Discover all tables and views in a database schema.
792
+
793
+ Returns a list of dicts with keys: name, type (table/view), column_count,
794
+ row_count_estimate, columns (list of column info dicts).
795
+ """
796
+ try:
797
+ from sqlalchemy import create_engine, text
798
+ from sqlalchemy import inspect as sa_inspect
799
+ except ImportError:
800
+ print(
801
+ "Error: sqlalchemy required. Install: pip install sqlalchemy",
802
+ file=sys.stderr,
803
+ )
804
+ sys.exit(1)
805
+
806
+ engine = create_engine(connection_string)
807
+ inspector = sa_inspect(engine)
808
+
809
+ tables = []
810
+ # Get tables
811
+ for table_name in inspector.get_table_names(schema=schema):
812
+ columns = inspector.get_columns(table_name, schema=schema)
813
+ pk = inspector.get_pk_constraint(table_name, schema=schema)
814
+ fks = inspector.get_foreign_keys(table_name, schema=schema)
815
+ tables.append({
816
+ "name": table_name,
817
+ "schema": schema,
818
+ "type": "table",
819
+ "column_count": len(columns),
820
+ "columns": [
821
+ {
822
+ "name": c["name"],
823
+ "sql_type": type(c["type"]).__name__.upper(),
824
+ "nullable": c.get("nullable", True),
825
+ "is_pk": c["name"] in (pk.get("constrained_columns", []) if pk else []),
826
+ }
827
+ for c in columns
828
+ ],
829
+ "primary_key": pk.get("constrained_columns", []) if pk else [],
830
+ "foreign_keys": [
831
+ {
832
+ "columns": fk["constrained_columns"],
833
+ "referred_table": fk["referred_table"],
834
+ "referred_columns": fk["referred_columns"],
835
+ }
836
+ for fk in fks
837
+ ],
838
+ })
839
+
840
+ # Get views
841
+ for view_name in inspector.get_view_names(schema=schema):
842
+ columns = inspector.get_columns(view_name, schema=schema)
843
+ tables.append({
844
+ "name": view_name,
845
+ "schema": schema,
846
+ "type": "view",
847
+ "column_count": len(columns),
848
+ "columns": [
849
+ {
850
+ "name": c["name"],
851
+ "sql_type": type(c["type"]).__name__.upper(),
852
+ "nullable": c.get("nullable", True),
853
+ "is_pk": False,
854
+ }
855
+ for c in columns
856
+ ],
857
+ "primary_key": [],
858
+ "foreign_keys": [],
859
+ })
860
+
861
+ engine.dispose()
862
+ return tables
863
+
864
+
865
+ def extract_db_table(
866
+ connection_string: str,
867
+ table_name: str,
868
+ schema: str | None = None,
869
+ sample: int = 0,
870
+ ) -> list[ExtractedSection]:
871
+ """Extract schema from a DB table with optional data sampling.
872
+
873
+ Args:
874
+ connection_string: SQLAlchemy connection string
875
+ table_name: Table or view name
876
+ schema: Optional DB schema (e.g., 'dbo', 'public')
877
+ sample: Number of rows to sample for type inference and format detection.
878
+ 0 = metadata only (original behavior).
879
+ """
880
+ try:
881
+ from sqlalchemy import create_engine
882
+ from sqlalchemy import inspect as sa_inspect
883
+ except ImportError:
884
+ print(
885
+ "Error: sqlalchemy required. Install: pip install sqlalchemy",
886
+ file=sys.stderr,
887
+ )
888
+ sys.exit(1)
889
+
890
+ engine = create_engine(connection_string)
891
+ inspector = sa_inspect(engine)
892
+
893
+ columns = inspector.get_columns(table_name, schema=schema)
894
+ pk = inspector.get_pk_constraint(table_name, schema=schema)
895
+ pk_cols = pk.get("constrained_columns", []) if pk else []
896
+ fks = inspector.get_foreign_keys(table_name, schema=schema)
897
+
898
+ # Sample rows if requested
899
+ sampled_rows: list[dict] = []
900
+ if sample > 0:
901
+ sampled_rows = _sample_rows(engine, table_name, schema, limit=sample)
902
+
903
+ section = ExtractedSection(name="Root")
904
+ embedded_sections: list[ExtractedSection] = []
905
+
906
+ for col in columns:
907
+ snake = to_snake_case(col["name"])
908
+ sql_type = type(col["type"]).__name__.upper()
909
+ py_type = SQL_TYPE_MAP.get(sql_type, "Any")
910
+ if col.get("nullable", True):
911
+ py_type = f"{py_type} | None"
912
+
913
+ # Build example from sampled data
914
+ example = f"SQL: {sql_type}"
915
+ embedded_fmt = None
916
+ if sampled_rows:
917
+ sample_vals = [
918
+ row.get(col["name"])
919
+ for row in sampled_rows
920
+ if row.get(col["name"]) is not None
921
+ ]
922
+ if sample_vals:
923
+ example = str(sample_vals[0])[:80]
924
+ # Check for embedded structured data in string columns
925
+ if sql_type in (
926
+ "VARCHAR", "NVARCHAR", "TEXT", "NTEXT",
927
+ "MEDIUMTEXT", "LONGTEXT", "CITEXT",
928
+ ):
929
+ for sv in sample_vals[:10]:
930
+ fmt = _detect_embedded_format(str(sv))
931
+ if fmt:
932
+ embedded_fmt = fmt
933
+ break
934
+
935
+ source_pattern = "db_column"
936
+ if col["name"] in pk_cols:
937
+ source_pattern = "db_column_pk"
938
+ if embedded_fmt:
939
+ source_pattern = f"db_column_embedded_{embedded_fmt}"
940
+ example = f"[embedded {embedded_fmt}] {example}"
941
+ # Parse the embedded content to extract nested schema
942
+ if embedded_fmt == "json" and sampled_rows:
943
+ for sv in sample_vals[:1]:
944
+ try:
945
+ parsed = json.loads(str(sv))
946
+ nested = extract_json_fields(
947
+ parsed, section=f"{col['name']} (Embedded JSON)"
948
+ )
949
+ embedded_sections.extend(nested)
950
+ except (json.JSONDecodeError, ValueError):
951
+ pass
952
+
953
+ section.fields.append(
954
+ ExtractedField(
955
+ name=snake,
956
+ python_type=py_type,
957
+ source_pattern=source_pattern,
958
+ example_value=example,
959
+ )
960
+ )
961
+
962
+ # Add FK annotations as comments in the section
963
+ for fk in fks:
964
+ fk_desc = (
965
+ f"FK: {','.join(fk['constrained_columns'])} -> "
966
+ f"{fk['referred_table']}({','.join(fk['referred_columns'])})"
967
+ )
968
+ section.fields.append(
969
+ ExtractedField(
970
+ name=f"_fk_{fk['referred_table']}",
971
+ python_type="# relationship",
972
+ source_pattern="db_foreign_key",
973
+ example_value=fk_desc,
974
+ )
975
+ )
976
+
977
+ engine.dispose()
978
+ result = []
979
+ if section.fields:
980
+ result.append(section)
981
+ result.extend(embedded_sections)
982
+ return result
983
+
984
+
985
+ # ---------------------------------------------------------------------------
986
+ # Deduplication
987
+ # ---------------------------------------------------------------------------
988
+
989
+
990
+ def deduplicate_fields(
991
+ fields: list[ExtractedField],
992
+ ) -> list[ExtractedField]:
993
+ seen: set[str] = set()
994
+ result: list[ExtractedField] = []
995
+ for f in fields:
996
+ if f.name not in seen:
997
+ seen.add(f.name)
998
+ result.append(f)
999
+ return result
1000
+
1001
+
1002
+ # ---------------------------------------------------------------------------
1003
+ # Model code generation (shared across all formats)
1004
+ # ---------------------------------------------------------------------------
1005
+
1006
+
1007
+ def generate_model_source(
1008
+ sections: list[ExtractedSection], root_class_name: str = "GeneratedPayload"
1009
+ ) -> str:
1010
+ lines = [
1011
+ '"""',
1012
+ "Auto-generated ingestion model.",
1013
+ "Generated by: extract_schema.py",
1014
+ "",
1015
+ "Review and adjust:",
1016
+ " - Field names and types",
1017
+ " - Which sections become nested models vs flat fields",
1018
+ " - Optional fields and defaults",
1019
+ '"""',
1020
+ "",
1021
+ "from __future__ import annotations",
1022
+ "",
1023
+ "from pydantic import BaseModel, ConfigDict, Field",
1024
+ "",
1025
+ ]
1026
+
1027
+ row_models: list[ExtractedSection] = []
1028
+ content_sections: list[ExtractedSection] = []
1029
+ for s in sections:
1030
+ if s.name.endswith("(Row Model)"):
1031
+ row_models.append(s)
1032
+ else:
1033
+ content_sections.append(s)
1034
+
1035
+ # Nested/row models first
1036
+ for rm in row_models:
1037
+ cname = to_class_name(rm.name.replace(" (Row Model)", "")) + "Row"
1038
+ if "(Item)" in rm.name:
1039
+ cname = (
1040
+ to_class_name(
1041
+ rm.name.replace(" (Item)", "").replace(
1042
+ " (Row Model)", ""
1043
+ )
1044
+ )
1045
+ + "Item"
1046
+ )
1047
+ rm_fields = deduplicate_fields(rm.fields)
1048
+ lines += [
1049
+ "",
1050
+ f"class {cname}(BaseModel):",
1051
+ f' """Row from {rm.name.replace(" (Row Model)", "")}."""',
1052
+ "",
1053
+ ]
1054
+ if not rm_fields:
1055
+ lines.append(" pass")
1056
+ else:
1057
+ for f in rm_fields:
1058
+ comment = (
1059
+ f" # e.g., {f.example_value}" if f.example_value else ""
1060
+ )
1061
+ lines.append(f" {f.name}: {f.python_type}{comment}")
1062
+ lines.append("")
1063
+
1064
+ # Root model
1065
+ all_fields: list[ExtractedField] = []
1066
+ section_markers: list[tuple[int, str]] = []
1067
+ for cs in content_sections:
1068
+ cs_fields = deduplicate_fields(cs.fields)
1069
+ if cs_fields:
1070
+ section_markers.append((len(all_fields), cs.name))
1071
+ all_fields.extend(cs_fields)
1072
+ all_fields = deduplicate_fields(all_fields)
1073
+
1074
+ lines += [
1075
+ "",
1076
+ f"class {root_class_name}(BaseModel):",
1077
+ f' """1:1 representation of source payload."""',
1078
+ "",
1079
+ ' model_config = ConfigDict(extra="allow")',
1080
+ "",
1081
+ ]
1082
+
1083
+ if not all_fields:
1084
+ lines.append(" pass")
1085
+ else:
1086
+ marker_idx = 0
1087
+ for i, f in enumerate(all_fields):
1088
+ if marker_idx < len(section_markers):
1089
+ fidx, sname = section_markers[marker_idx]
1090
+ if i >= fidx:
1091
+ lines.append(f" # --- {sname} ---")
1092
+ marker_idx += 1
1093
+ comment = (
1094
+ f" # e.g., {f.example_value}" if f.example_value else ""
1095
+ )
1096
+ lines.append(f" {f.name}: {f.python_type}{comment}")
1097
+
1098
+ return "\n".join(lines) + "\n"
1099
+
1100
+
1101
+ # ---------------------------------------------------------------------------
1102
+ # Schema evolution
1103
+ # ---------------------------------------------------------------------------
1104
+
1105
+
1106
+ def sections_to_schema_dict(
1107
+ sections: list[ExtractedSection], source: str, fmt: str
1108
+ ) -> dict:
1109
+ fields = []
1110
+ for s in sections:
1111
+ for f in s.fields:
1112
+ fields.append(
1113
+ {
1114
+ "name": f.name,
1115
+ "type": f.python_type,
1116
+ "source_pattern": f.source_pattern,
1117
+ "section": s.name,
1118
+ }
1119
+ )
1120
+ return {
1121
+ "extracted_at": datetime.now(timezone.utc).isoformat(),
1122
+ "source_file": source,
1123
+ "format": fmt,
1124
+ "field_count": len(fields),
1125
+ "fields": fields,
1126
+ }
1127
+
1128
+
1129
+ def save_baseline(
1130
+ sections: list[ExtractedSection], path: Path, source: str, fmt: str
1131
+ ) -> None:
1132
+ schema = sections_to_schema_dict(sections, source, fmt)
1133
+ path.parent.mkdir(parents=True, exist_ok=True)
1134
+ path.write_text(json.dumps(schema, indent=2), encoding="utf-8")
1135
+ print(
1136
+ f"Baseline saved: {path} ({schema['field_count']} fields)",
1137
+ file=sys.stderr,
1138
+ )
1139
+
1140
+
1141
+ def diff_baseline(
1142
+ sections: list[ExtractedSection], baseline_path: Path
1143
+ ) -> list[SchemaChange]:
1144
+ baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
1145
+ old_fields = {f["name"]: f["type"] for f in baseline["fields"]}
1146
+ new_fields: dict[str, str] = {}
1147
+ for s in sections:
1148
+ for f in s.fields:
1149
+ if f.name not in new_fields:
1150
+ new_fields[f.name] = f.python_type
1151
+
1152
+ changes: list[SchemaChange] = []
1153
+ for name in sorted(set(old_fields) | set(new_fields)):
1154
+ if name not in old_fields:
1155
+ changes.append(
1156
+ SchemaChange(name, "added", new_value=new_fields[name])
1157
+ )
1158
+ elif name not in new_fields:
1159
+ changes.append(
1160
+ SchemaChange(name, "removed", old_value=old_fields[name])
1161
+ )
1162
+ elif old_fields[name] != new_fields[name]:
1163
+ changes.append(
1164
+ SchemaChange(
1165
+ name,
1166
+ "type_changed",
1167
+ old_value=old_fields[name],
1168
+ new_value=new_fields[name],
1169
+ )
1170
+ )
1171
+ return changes
1172
+
1173
+
1174
+ # ---------------------------------------------------------------------------
1175
+ # CLI
1176
+ # ---------------------------------------------------------------------------
1177
+
1178
+
1179
+ def main() -> None:
1180
+ import argparse
1181
+
1182
+ parser = argparse.ArgumentParser(
1183
+ description="Unified schema extractor — any format to Pydantic"
1184
+ )
1185
+ parser.add_argument(
1186
+ "source",
1187
+ type=Path,
1188
+ nargs="?",
1189
+ default=None,
1190
+ help="Path to source data file",
1191
+ )
1192
+ parser.add_argument(
1193
+ "--format",
1194
+ dest="fmt",
1195
+ default="auto",
1196
+ choices=["auto", "json", "md", "csv", "xlsx", "yaml", "db", "text"],
1197
+ help="Source format (default: auto-detect from extension)",
1198
+ )
1199
+ parser.add_argument(
1200
+ "--class-name",
1201
+ default="GeneratedPayload",
1202
+ help="Root class name",
1203
+ )
1204
+ parser.add_argument(
1205
+ "--output",
1206
+ type=Path,
1207
+ default=None,
1208
+ help="Output .py file path",
1209
+ )
1210
+ parser.add_argument(
1211
+ "--show-sections",
1212
+ action="store_true",
1213
+ help="Print extracted sections instead of code",
1214
+ )
1215
+ parser.add_argument(
1216
+ "--save-baseline",
1217
+ type=Path,
1218
+ default=None,
1219
+ help="Save schema snapshot to JSON",
1220
+ )
1221
+ parser.add_argument(
1222
+ "--diff",
1223
+ type=Path,
1224
+ default=None,
1225
+ help="Diff current schema against baseline JSON",
1226
+ )
1227
+ parser.add_argument(
1228
+ "--connection-string",
1229
+ default=None,
1230
+ help="DB connection string (for --format db)",
1231
+ )
1232
+ parser.add_argument(
1233
+ "--table",
1234
+ default=None,
1235
+ help="DB table name (for --format db)",
1236
+ )
1237
+ parser.add_argument(
1238
+ "--schema",
1239
+ default=None,
1240
+ help="DB schema name, e.g. 'dbo', 'public' (for --format db)",
1241
+ )
1242
+ parser.add_argument(
1243
+ "--discover",
1244
+ action="store_true",
1245
+ help="Discover all tables/views in the database (for --format db)",
1246
+ )
1247
+ parser.add_argument(
1248
+ "--sample",
1249
+ type=int,
1250
+ default=0,
1251
+ help="Number of rows to sample for type inference and embedded format detection (for --format db, default: 0 = metadata only)",
1252
+ )
1253
+ args = parser.parse_args()
1254
+
1255
+ fmt = detect_format(args.source, args.fmt)
1256
+
1257
+ if fmt == "db":
1258
+ if not args.connection_string:
1259
+ print(
1260
+ "Error: --connection-string required for db format",
1261
+ file=sys.stderr,
1262
+ )
1263
+ sys.exit(1)
1264
+
1265
+ # Discovery mode: list all tables and their schemas
1266
+ if args.discover:
1267
+ tables = discover_db_tables(args.connection_string, schema=args.schema)
1268
+ print(f"Discovered {len(tables)} tables/views:\n")
1269
+ for t in tables:
1270
+ pk_str = f" PK: {', '.join(t['primary_key'])}" if t['primary_key'] else ""
1271
+ fk_str = f" FKs: {len(t['foreign_keys'])}" if t['foreign_keys'] else ""
1272
+ print(f" [{t['type'].upper()}] {t['name']} ({t['column_count']} columns){pk_str}{fk_str}")
1273
+ for col in t['columns']:
1274
+ pk_marker = " [PK]" if col['is_pk'] else ""
1275
+ null_marker = " NULL" if col['nullable'] else " NOT NULL"
1276
+ print(f" {col['name']}: {col['sql_type']}{null_marker}{pk_marker}")
1277
+ if t['foreign_keys']:
1278
+ for fk in t['foreign_keys']:
1279
+ print(f" FK: {','.join(fk['columns'])} -> {fk['referred_table']}({','.join(fk['referred_columns'])})")
1280
+ print()
1281
+ sys.exit(0)
1282
+
1283
+ if not args.table:
1284
+ print(
1285
+ "Error: --table required for db format (or use --discover to list tables)",
1286
+ file=sys.stderr,
1287
+ )
1288
+ sys.exit(1)
1289
+ sections = extract_db_table(
1290
+ args.connection_string, args.table,
1291
+ schema=args.schema, sample=args.sample,
1292
+ )
1293
+ source_label = f"{args.table} (db)"
1294
+ else:
1295
+ if not args.source or not args.source.exists():
1296
+ print(
1297
+ f"Error: File not found: {args.source}", file=sys.stderr
1298
+ )
1299
+ sys.exit(1)
1300
+ source_label = str(args.source)
1301
+
1302
+ if fmt == "xlsx":
1303
+ sections = extract_xlsx(args.source)
1304
+ else:
1305
+ text = args.source.read_text(encoding="utf-8")
1306
+ extractors = {
1307
+ "json": lambda t: extract_json_fields(json.loads(t)),
1308
+ "md": extract_markdown,
1309
+ "csv": extract_csv,
1310
+ "yaml": extract_yaml,
1311
+ "text": extract_text,
1312
+ }
1313
+ sections = extractors[fmt](text)
1314
+
1315
+ if args.save_baseline:
1316
+ save_baseline(sections, args.save_baseline, source_label, fmt)
1317
+
1318
+ if args.diff:
1319
+ if not args.diff.exists():
1320
+ print(
1321
+ f"Error: Baseline not found: {args.diff}", file=sys.stderr
1322
+ )
1323
+ sys.exit(1)
1324
+ changes = diff_baseline(sections, args.diff)
1325
+ if not changes:
1326
+ print("No schema changes detected.")
1327
+ else:
1328
+ print(f"{len(changes)} schema change(s) detected:\n")
1329
+ for c in changes:
1330
+ if c.change_type == "added":
1331
+ print(f" + ADDED {c.field_name}: {c.new_value}")
1332
+ elif c.change_type == "removed":
1333
+ print(f" - REMOVED {c.field_name}: {c.old_value}")
1334
+ else:
1335
+ print(
1336
+ f" ~ CHANGED {c.field_name}: {c.old_value} -> {c.new_value}"
1337
+ )
1338
+ sys.exit(0)
1339
+
1340
+ if args.show_sections:
1341
+ print(f"Extracted {len(sections)} sections:\n")
1342
+ for s in sections:
1343
+ print(f" [{s.name}] -- {len(s.fields)} fields")
1344
+ for f in s.fields:
1345
+ print(
1346
+ f" {f.name}: {f.python_type} ({f.source_pattern}) = {f.example_value!r}"
1347
+ )
1348
+ print()
1349
+ sys.exit(0)
1350
+
1351
+ source_code = generate_model_source(
1352
+ sections, root_class_name=args.class_name
1353
+ )
1354
+ if args.output:
1355
+ args.output.parent.mkdir(parents=True, exist_ok=True)
1356
+ args.output.write_text(source_code, encoding="utf-8")
1357
+ print(f"Written to {args.output}")
1358
+ else:
1359
+ print(source_code)
1360
+
1361
+
1362
+ if __name__ == "__main__":
1363
+ main()