@useatlas/create 0.0.6 → 0.0.7

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 (952) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/index.ts +253 -36
  4. package/package.json +4 -4
  5. package/templates/docker/Dockerfile +1 -1
  6. package/templates/docker/Dockerfile.sidecar +1 -1
  7. package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +17 -14
  8. package/templates/docker/bin/__tests__/failure-threshold.test.ts +148 -0
  9. package/templates/docker/bin/__tests__/fatal-error-propagation.test.ts +267 -0
  10. package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +5 -5
  11. package/templates/docker/bin/__tests__/schema-drift.test.ts +39 -0
  12. package/templates/docker/bin/atlas.ts +981 -1819
  13. package/templates/docker/bin/benchmark.ts +14 -16
  14. package/templates/docker/bin/enrich.ts +7 -2
  15. package/templates/docker/brand.css +13 -0
  16. package/templates/docker/data/cybersec-semantic/catalog.yml +222 -0
  17. package/templates/docker/data/cybersec-semantic/entities/alerts.yml +195 -0
  18. package/templates/docker/data/cybersec-semantic/entities/assets.yml +191 -0
  19. package/templates/docker/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
  20. package/templates/docker/data/cybersec-semantic/entities/incidents.yml +219 -0
  21. package/templates/docker/data/cybersec-semantic/entities/organizations.yml +136 -0
  22. package/templates/docker/data/cybersec-semantic/entities/plans.yml +114 -0
  23. package/templates/docker/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
  24. package/templates/docker/data/cybersec-semantic/entities/scan_results.yml +215 -0
  25. package/templates/docker/data/cybersec-semantic/entities/scans.yml +180 -0
  26. package/templates/docker/data/cybersec-semantic/entities/subscriptions.yml +184 -0
  27. package/templates/docker/data/cybersec-semantic/entities/users.yml +140 -0
  28. package/templates/docker/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
  29. package/templates/docker/data/cybersec-semantic/glossary.yml +207 -0
  30. package/templates/docker/data/cybersec-semantic/metrics/business.yml +148 -0
  31. package/templates/docker/data/cybersec-semantic/metrics/compliance.yml +138 -0
  32. package/templates/docker/data/cybersec-semantic/metrics/security.yml +181 -0
  33. package/templates/docker/data/cybersec.sql +8 -8
  34. package/templates/docker/data/demo.sql +3 -0
  35. package/templates/docker/data/ecommerce-semantic/catalog.yml +221 -0
  36. package/templates/docker/data/ecommerce-semantic/entities/categories.yml +91 -0
  37. package/templates/docker/data/ecommerce-semantic/entities/customers.yml +133 -0
  38. package/templates/docker/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
  39. package/templates/docker/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
  40. package/templates/docker/data/ecommerce-semantic/entities/order_items.yml +159 -0
  41. package/templates/docker/data/ecommerce-semantic/entities/orders.yml +199 -0
  42. package/templates/docker/data/ecommerce-semantic/entities/payments.yml +140 -0
  43. package/templates/docker/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
  44. package/templates/docker/data/ecommerce-semantic/entities/products.yml +178 -0
  45. package/templates/docker/data/ecommerce-semantic/entities/promotions.yml +171 -0
  46. package/templates/docker/data/ecommerce-semantic/entities/returns.yml +144 -0
  47. package/templates/docker/data/ecommerce-semantic/entities/sellers.yml +124 -0
  48. package/templates/docker/data/ecommerce-semantic/entities/shipments.yml +159 -0
  49. package/templates/docker/data/ecommerce-semantic/glossary.yml +193 -0
  50. package/templates/docker/data/ecommerce-semantic/metrics/customers.yml +116 -0
  51. package/templates/docker/data/ecommerce-semantic/metrics/operations.yml +131 -0
  52. package/templates/docker/data/ecommerce-semantic/metrics/revenue.yml +120 -0
  53. package/templates/docker/docs/deploy.md +2 -1
  54. package/templates/docker/ee/src/__mocks__/internal.ts +170 -0
  55. package/templates/docker/ee/src/audit/purge-scheduler.ts +113 -0
  56. package/templates/docker/ee/src/audit/retention.ts +467 -0
  57. package/templates/docker/ee/src/auth/ip-allowlist.ts +367 -0
  58. package/templates/docker/ee/src/auth/roles.ts +562 -0
  59. package/templates/docker/ee/src/auth/scim.ts +343 -0
  60. package/templates/docker/ee/src/auth/sso.ts +538 -0
  61. package/templates/docker/ee/src/backups/engine.ts +355 -0
  62. package/templates/docker/ee/src/backups/index.ts +26 -0
  63. package/templates/docker/ee/src/backups/restore.ts +169 -0
  64. package/templates/docker/ee/src/backups/scheduler.ts +153 -0
  65. package/templates/docker/ee/src/backups/verify.ts +124 -0
  66. package/templates/docker/ee/src/branding/white-label.ts +228 -0
  67. package/templates/docker/ee/src/compliance/masking.ts +477 -0
  68. package/templates/docker/ee/src/compliance/patterns.ts +16 -0
  69. package/templates/docker/ee/src/compliance/pii-detection.ts +217 -0
  70. package/templates/docker/ee/src/compliance/reports.ts +402 -0
  71. package/templates/docker/ee/src/deploy-mode.ts +37 -0
  72. package/templates/docker/ee/src/governance/approval.ts +699 -0
  73. package/templates/docker/ee/src/index.ts +74 -0
  74. package/templates/docker/ee/src/platform/domains.ts +562 -0
  75. package/templates/docker/ee/src/platform/model-routing.ts +382 -0
  76. package/templates/docker/ee/src/platform/residency.ts +265 -0
  77. package/templates/docker/ee/src/sla/alerting.ts +382 -0
  78. package/templates/docker/ee/src/sla/index.ts +12 -0
  79. package/templates/docker/ee/src/sla/metrics.ts +275 -0
  80. package/templates/docker/ee/src/test-setup.ts +1 -0
  81. package/templates/docker/next.config.ts +4 -1
  82. package/templates/docker/package.json +49 -29
  83. package/templates/docker/sidecar/Dockerfile +1 -1
  84. package/templates/docker/src/api/index.ts +336 -24
  85. package/templates/docker/src/api/routes/actions.ts +443 -176
  86. package/templates/docker/src/api/routes/admin-abuse.ts +219 -0
  87. package/templates/docker/src/api/routes/admin-approval.ts +418 -0
  88. package/templates/docker/src/api/routes/admin-audit-retention.ts +405 -0
  89. package/templates/docker/src/api/routes/admin-auth.ts +122 -0
  90. package/templates/docker/src/api/routes/admin-branding.ts +252 -0
  91. package/templates/docker/src/api/routes/admin-compliance.ts +352 -0
  92. package/templates/docker/src/api/routes/admin-domains.ts +334 -0
  93. package/templates/docker/src/api/routes/admin-integrations.ts +2667 -0
  94. package/templates/docker/src/api/routes/admin-ip-allowlist.ts +261 -0
  95. package/templates/docker/src/api/routes/admin-learned-patterns.ts +525 -0
  96. package/templates/docker/src/api/routes/admin-model-config.ts +252 -0
  97. package/templates/docker/src/api/routes/admin-onboarding-emails.ts +145 -0
  98. package/templates/docker/src/api/routes/admin-orgs.ts +710 -0
  99. package/templates/docker/src/api/routes/admin-prompts.ts +694 -0
  100. package/templates/docker/src/api/routes/admin-residency.ts +570 -0
  101. package/templates/docker/src/api/routes/admin-roles.ts +296 -0
  102. package/templates/docker/src/api/routes/admin-router.ts +120 -0
  103. package/templates/docker/src/api/routes/admin-sandbox.ts +417 -0
  104. package/templates/docker/src/api/routes/admin-scim.ts +262 -0
  105. package/templates/docker/src/api/routes/admin-sso.ts +545 -0
  106. package/templates/docker/src/api/routes/admin-suggestions.ts +176 -0
  107. package/templates/docker/src/api/routes/admin-usage.ts +310 -0
  108. package/templates/docker/src/api/routes/admin.ts +4156 -898
  109. package/templates/docker/src/api/routes/auth-preamble.ts +105 -0
  110. package/templates/docker/src/api/routes/billing.ts +397 -0
  111. package/templates/docker/src/api/routes/chat.ts +597 -334
  112. package/templates/docker/src/api/routes/conversations.ts +987 -132
  113. package/templates/docker/src/api/routes/demo.ts +673 -0
  114. package/templates/docker/src/api/routes/discord.ts +274 -0
  115. package/templates/docker/src/api/routes/ee-error-handler.ts +32 -0
  116. package/templates/docker/src/api/routes/health.ts +129 -14
  117. package/templates/docker/src/api/routes/middleware.ts +244 -0
  118. package/templates/docker/src/api/routes/onboarding-emails.ts +134 -0
  119. package/templates/docker/src/api/routes/onboarding.ts +1109 -0
  120. package/templates/docker/src/api/routes/openapi.ts +184 -1597
  121. package/templates/docker/src/api/routes/platform-admin.ts +760 -0
  122. package/templates/docker/src/api/routes/platform-backups.ts +436 -0
  123. package/templates/docker/src/api/routes/platform-domains.ts +235 -0
  124. package/templates/docker/src/api/routes/platform-residency.ts +257 -0
  125. package/templates/docker/src/api/routes/platform-sla.ts +379 -0
  126. package/templates/docker/src/api/routes/prompts.ts +221 -0
  127. package/templates/docker/src/api/routes/public-branding.ts +106 -0
  128. package/templates/docker/src/api/routes/query.ts +330 -219
  129. package/templates/docker/src/api/routes/scheduled-tasks.ts +393 -297
  130. package/templates/docker/src/api/routes/semantic.ts +179 -0
  131. package/templates/docker/src/api/routes/sessions.ts +210 -0
  132. package/templates/docker/src/api/routes/shared-domains.ts +98 -0
  133. package/templates/docker/src/api/routes/shared-schemas.ts +139 -0
  134. package/templates/docker/src/api/routes/slack.ts +209 -52
  135. package/templates/docker/src/api/routes/suggestions.ts +233 -0
  136. package/templates/docker/src/api/routes/tables.ts +67 -0
  137. package/templates/docker/src/api/routes/teams.ts +222 -0
  138. package/templates/docker/src/api/routes/validate-sql.ts +188 -0
  139. package/templates/docker/src/api/routes/validation-hook.ts +62 -0
  140. package/templates/docker/src/api/routes/widget-loader.ts +356 -0
  141. package/templates/docker/src/api/routes/widget.ts +428 -0
  142. package/templates/docker/src/api/routes/wizard.ts +852 -0
  143. package/templates/docker/src/api/server.ts +187 -69
  144. package/templates/docker/src/app/error.tsx +5 -2
  145. package/templates/docker/src/app/globals.css +1 -1
  146. package/templates/docker/src/app/layout.tsx +7 -2
  147. package/templates/docker/src/app/page.tsx +39 -5
  148. package/templates/docker/src/components/data-table/data-table-column-header.tsx +99 -0
  149. package/templates/docker/src/components/data-table/data-table-date-filter.tsx +225 -0
  150. package/templates/docker/src/components/data-table/data-table-expandable.tsx +125 -0
  151. package/templates/docker/src/components/data-table/data-table-faceted-filter.tsx +189 -0
  152. package/templates/docker/src/components/data-table/data-table-pagination.tsx +112 -0
  153. package/templates/docker/src/components/data-table/data-table-range-filter.tsx +122 -0
  154. package/templates/docker/src/components/data-table/data-table-slider-filter.tsx +256 -0
  155. package/templates/docker/src/components/data-table/data-table-sort-list.tsx +407 -0
  156. package/templates/docker/src/components/data-table/data-table-toolbar.tsx +149 -0
  157. package/templates/docker/src/components/data-table/data-table-view-options.tsx +89 -0
  158. package/templates/docker/src/components/data-table/data-table.tsx +105 -0
  159. package/templates/docker/src/components/form-dialog.tsx +135 -0
  160. package/templates/docker/src/components/ui/accordion.tsx +66 -0
  161. package/templates/docker/src/components/ui/calendar.tsx +220 -0
  162. package/templates/docker/src/components/ui/checkbox.tsx +32 -0
  163. package/templates/docker/src/components/ui/faceted.tsx +283 -0
  164. package/templates/docker/src/components/ui/form.tsx +167 -0
  165. package/templates/docker/src/components/ui/label.tsx +24 -0
  166. package/templates/docker/src/components/ui/popover.tsx +89 -0
  167. package/templates/docker/src/components/ui/progress.tsx +31 -0
  168. package/templates/docker/src/components/ui/scroll-area.tsx +6 -2
  169. package/templates/docker/src/components/ui/slider.tsx +63 -0
  170. package/templates/docker/src/components/ui/sortable.tsx +581 -0
  171. package/templates/docker/src/components/ui/switch.tsx +35 -0
  172. package/templates/docker/src/components/ui/textarea.tsx +18 -0
  173. package/templates/docker/src/config/data-table.ts +82 -0
  174. package/templates/docker/src/env-check.ts +74 -0
  175. package/templates/docker/src/hooks/use-callback-ref.ts +27 -0
  176. package/templates/docker/src/hooks/use-data-table.ts +316 -0
  177. package/templates/docker/src/hooks/use-debounced-callback.ts +28 -0
  178. package/templates/docker/src/lib/action-types.ts +7 -41
  179. package/templates/docker/src/lib/agent-query.ts +4 -2
  180. package/templates/docker/src/lib/agent.ts +363 -31
  181. package/templates/docker/src/lib/auth/admin-permissions.ts +38 -0
  182. package/templates/docker/src/lib/auth/audit.ts +19 -4
  183. package/templates/docker/src/lib/auth/byot.ts +3 -3
  184. package/templates/docker/src/lib/auth/client.ts +33 -3
  185. package/templates/docker/src/lib/auth/detect.ts +29 -8
  186. package/templates/docker/src/lib/auth/managed.ts +104 -14
  187. package/templates/docker/src/lib/auth/middleware.ts +53 -6
  188. package/templates/docker/src/lib/auth/migrate.ts +140 -15
  189. package/templates/docker/src/lib/auth/oauth-state.ts +123 -0
  190. package/templates/docker/src/lib/auth/org-permissions.ts +55 -0
  191. package/templates/docker/src/lib/auth/permissions.ts +26 -19
  192. package/templates/docker/src/lib/auth/server.ts +355 -9
  193. package/templates/docker/src/lib/auth/simple-key.ts +3 -3
  194. package/templates/docker/src/lib/auth/types.ts +15 -21
  195. package/templates/docker/src/lib/billing/enforcement.ts +368 -0
  196. package/templates/docker/src/lib/billing/plans.ts +155 -0
  197. package/templates/docker/src/lib/cache/index.ts +92 -0
  198. package/templates/docker/src/lib/cache/keys.ts +30 -0
  199. package/templates/docker/src/lib/cache/lru.ts +79 -0
  200. package/templates/docker/src/lib/cache/types.ts +31 -0
  201. package/templates/docker/src/lib/compose-refs.ts +62 -0
  202. package/templates/docker/src/lib/config.ts +563 -11
  203. package/templates/docker/src/lib/connection-types.ts +9 -0
  204. package/templates/docker/src/lib/conversation-types.ts +1 -25
  205. package/templates/docker/src/lib/conversations.ts +345 -14
  206. package/templates/docker/src/lib/data-table.ts +61 -0
  207. package/templates/docker/src/lib/db/connection.ts +793 -39
  208. package/templates/docker/src/lib/db/internal.ts +985 -139
  209. package/templates/docker/src/lib/db/migrate.ts +295 -0
  210. package/templates/docker/src/lib/db/migrations/0000_baseline.sql +703 -0
  211. package/templates/docker/src/lib/db/migrations/0001_teams_installations.sql +14 -0
  212. package/templates/docker/src/lib/db/migrations/0002_discord_installations.sql +14 -0
  213. package/templates/docker/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
  214. package/templates/docker/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
  215. package/templates/docker/src/lib/db/migrations/0005_oauth_state.sql +16 -0
  216. package/templates/docker/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
  217. package/templates/docker/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
  218. package/templates/docker/src/lib/db/migrations/0008_github_installations.sql +14 -0
  219. package/templates/docker/src/lib/db/migrations/0009_linear_installations.sql +15 -0
  220. package/templates/docker/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
  221. package/templates/docker/src/lib/db/migrations/0011_email_installations.sql +16 -0
  222. package/templates/docker/src/lib/db/migrations/0012_region_migrations.sql +25 -0
  223. package/templates/docker/src/lib/db/schema.ts +1120 -0
  224. package/templates/docker/src/lib/db/source-rate-limit.ts +89 -139
  225. package/templates/docker/src/lib/demo.ts +308 -0
  226. package/templates/docker/src/lib/discord/store.ts +225 -0
  227. package/templates/docker/src/lib/effect/ai.ts +243 -0
  228. package/templates/docker/src/lib/effect/errors.ts +234 -0
  229. package/templates/docker/src/lib/effect/hono.ts +454 -0
  230. package/templates/docker/src/lib/effect/index.ts +137 -0
  231. package/templates/docker/src/lib/effect/layers.ts +496 -0
  232. package/templates/docker/src/lib/effect/services.ts +776 -0
  233. package/templates/docker/src/lib/effect/sql.ts +178 -0
  234. package/templates/docker/src/lib/effect/toolkit.ts +123 -0
  235. package/templates/docker/src/lib/email/delivery.ts +232 -0
  236. package/templates/docker/src/lib/email/engine.ts +349 -0
  237. package/templates/docker/src/lib/email/hooks.ts +107 -0
  238. package/templates/docker/src/lib/email/index.ts +16 -0
  239. package/templates/docker/src/lib/email/scheduler.ts +72 -0
  240. package/templates/docker/src/lib/email/sequence.ts +73 -0
  241. package/templates/docker/src/lib/email/store.ts +163 -0
  242. package/templates/docker/src/lib/email/templates.ts +215 -0
  243. package/templates/docker/src/lib/format.ts +67 -0
  244. package/templates/docker/src/lib/gchat/store.ts +202 -0
  245. package/templates/docker/src/lib/github/store.ts +197 -0
  246. package/templates/docker/src/lib/id.ts +29 -0
  247. package/templates/docker/src/lib/integrations/types.ts +166 -0
  248. package/templates/docker/src/lib/learn/pattern-analyzer.ts +224 -0
  249. package/templates/docker/src/lib/learn/pattern-cache.ts +229 -0
  250. package/templates/docker/src/lib/learn/pattern-proposer.ts +87 -0
  251. package/templates/docker/src/lib/learn/suggestion-helpers.ts +34 -0
  252. package/templates/docker/src/lib/learn/suggestions.ts +139 -0
  253. package/templates/docker/src/lib/linear/store.ts +200 -0
  254. package/templates/docker/src/lib/logger.ts +35 -3
  255. package/templates/docker/src/lib/metering.ts +272 -0
  256. package/templates/docker/src/lib/parsers.ts +99 -0
  257. package/templates/docker/src/lib/plugins/hooks.ts +13 -11
  258. package/templates/docker/src/lib/plugins/index.ts +3 -1
  259. package/templates/docker/src/lib/plugins/registry.ts +58 -6
  260. package/templates/docker/src/lib/plugins/settings.ts +147 -0
  261. package/templates/docker/src/lib/plugins/wiring.ts +6 -9
  262. package/templates/docker/src/lib/profiler.ts +1665 -0
  263. package/templates/docker/src/lib/providers.ts +188 -13
  264. package/templates/docker/src/lib/rls.ts +172 -60
  265. package/templates/docker/src/lib/sandbox/credentials.ts +206 -0
  266. package/templates/docker/src/lib/sandbox/validate.ts +179 -0
  267. package/templates/docker/src/lib/scheduled-task-types.ts +26 -94
  268. package/templates/docker/src/lib/scheduled-tasks.ts +174 -34
  269. package/templates/docker/src/lib/scheduler/delivery.ts +248 -150
  270. package/templates/docker/src/lib/scheduler/engine.ts +190 -154
  271. package/templates/docker/src/lib/scheduler/executor.ts +74 -23
  272. package/templates/docker/src/lib/scheduler/preview.ts +72 -0
  273. package/templates/docker/src/lib/security/abuse.ts +463 -0
  274. package/templates/docker/src/lib/semantic/diff.ts +267 -0
  275. package/templates/docker/src/lib/semantic/entities.ts +167 -0
  276. package/templates/docker/src/lib/semantic/files.ts +283 -0
  277. package/templates/docker/src/lib/semantic/index.ts +27 -0
  278. package/templates/docker/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
  279. package/templates/docker/src/lib/semantic/sync.ts +581 -0
  280. package/templates/docker/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
  281. package/templates/docker/src/lib/settings.ts +817 -0
  282. package/templates/docker/src/lib/sidecar-types.ts +13 -0
  283. package/templates/docker/src/lib/slack/store.ts +134 -25
  284. package/templates/docker/src/lib/startup.ts +528 -362
  285. package/templates/docker/src/lib/teams/store.ts +216 -0
  286. package/templates/docker/src/lib/telegram/store.ts +202 -0
  287. package/templates/docker/src/lib/telemetry.ts +40 -0
  288. package/templates/docker/src/lib/tools/actions/audit.ts +8 -5
  289. package/templates/docker/src/lib/tools/actions/email.ts +3 -1
  290. package/templates/docker/src/lib/tools/actions/handler.ts +276 -93
  291. package/templates/docker/src/lib/tools/actions/jira.ts +2 -2
  292. package/templates/docker/src/lib/tools/backends/detect.ts +16 -0
  293. package/templates/docker/src/lib/tools/backends/index.ts +11 -0
  294. package/templates/docker/src/lib/tools/backends/nsjail.ts +213 -0
  295. package/templates/docker/src/lib/tools/backends/shared.ts +103 -0
  296. package/templates/docker/src/lib/tools/backends/types.ts +26 -0
  297. package/templates/docker/src/lib/tools/explore-nsjail.ts +7 -228
  298. package/templates/docker/src/lib/tools/explore-sandbox.ts +4 -29
  299. package/templates/docker/src/lib/tools/explore-sidecar.ts +18 -2
  300. package/templates/docker/src/lib/tools/explore.ts +246 -54
  301. package/templates/docker/src/lib/tools/index.ts +17 -0
  302. package/templates/docker/src/lib/tools/python-nsjail.ts +11 -139
  303. package/templates/docker/src/lib/tools/python-sandbox.ts +9 -132
  304. package/templates/docker/src/lib/tools/python-sidecar.ts +184 -3
  305. package/templates/docker/src/lib/tools/python-stream.ts +33 -0
  306. package/templates/docker/src/lib/tools/python-wrapper.ts +129 -0
  307. package/templates/docker/src/lib/tools/python.ts +115 -15
  308. package/templates/docker/src/lib/tools/registry.ts +14 -2
  309. package/templates/docker/src/lib/tools/sql.ts +778 -362
  310. package/templates/docker/src/lib/tracing.ts +16 -0
  311. package/templates/docker/src/lib/whatsapp/store.ts +198 -0
  312. package/templates/docker/src/lib/workspace.ts +89 -0
  313. package/templates/docker/src/progress.ts +121 -0
  314. package/templates/docker/src/types/data-table.ts +48 -0
  315. package/templates/docker/src/ui/atlas-chat-reexport.ts +3 -0
  316. package/templates/docker/src/ui/components/actions/action-approval-card.tsx +26 -19
  317. package/templates/docker/src/ui/components/actions/action-status-badge.tsx +3 -3
  318. package/templates/docker/src/ui/components/admin/admin-layout.tsx +57 -39
  319. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +213 -35
  320. package/templates/docker/src/ui/components/admin/delivery-status-badge.tsx +53 -0
  321. package/templates/docker/src/ui/components/admin/empty-state.tsx +27 -6
  322. package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -52
  323. package/templates/docker/src/ui/components/admin/error-banner.tsx +2 -2
  324. package/templates/docker/src/ui/components/admin/feature-disabled.tsx +28 -5
  325. package/templates/docker/src/ui/components/admin-content-wrapper.tsx +87 -0
  326. package/templates/docker/src/ui/components/atlas-chat.tsx +449 -166
  327. package/templates/docker/src/ui/components/branding-head.tsx +41 -0
  328. package/templates/docker/src/ui/components/chart/chart-detection.ts +62 -5
  329. package/templates/docker/src/ui/components/chart/result-chart.tsx +316 -125
  330. package/templates/docker/src/ui/components/chat/api-key-bar.tsx +4 -4
  331. package/templates/docker/src/ui/components/chat/data-table.tsx +45 -4
  332. package/templates/docker/src/ui/components/chat/error-banner.tsx +86 -5
  333. package/templates/docker/src/ui/components/chat/follow-up-chips.tsx +29 -0
  334. package/templates/docker/src/ui/components/chat/markdown.tsx +24 -0
  335. package/templates/docker/src/ui/components/chat/prompt-library.tsx +206 -0
  336. package/templates/docker/src/ui/components/chat/python-result-card.tsx +106 -78
  337. package/templates/docker/src/ui/components/chat/result-card-base.tsx +101 -0
  338. package/templates/docker/src/ui/components/chat/share-dialog.tsx +377 -0
  339. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +94 -73
  340. package/templates/docker/src/ui/components/chat/suggestion-chips.tsx +46 -0
  341. package/templates/docker/src/ui/components/chat/tool-part.tsx +16 -4
  342. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +48 -17
  343. package/templates/docker/src/ui/components/conversations/conversation-list.tsx +38 -24
  344. package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
  345. package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +9 -2
  346. package/templates/docker/src/ui/components/error-boundary.tsx +66 -0
  347. package/templates/docker/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
  348. package/templates/docker/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
  349. package/templates/docker/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
  350. package/templates/docker/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
  351. package/templates/docker/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
  352. package/templates/docker/src/ui/components/notebook/notebook-cell.tsx +119 -0
  353. package/templates/docker/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
  354. package/templates/docker/src/ui/components/notebook/notebook-export.ts +287 -0
  355. package/templates/docker/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
  356. package/templates/docker/src/ui/components/notebook/notebook-shell.tsx +266 -0
  357. package/templates/docker/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
  358. package/templates/docker/src/ui/components/notebook/types.ts +39 -0
  359. package/templates/docker/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
  360. package/templates/docker/src/ui/components/notebook/use-notebook.ts +684 -0
  361. package/templates/docker/src/ui/components/org-switcher.tsx +111 -0
  362. package/templates/docker/src/ui/components/region-picker.tsx +103 -0
  363. package/templates/docker/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
  364. package/templates/docker/src/ui/components/social-icons.tsx +26 -0
  365. package/templates/docker/src/ui/components/tour/guided-tour.tsx +81 -0
  366. package/templates/docker/src/ui/components/tour/index.ts +5 -0
  367. package/templates/docker/src/ui/components/tour/nav-bar.tsx +100 -0
  368. package/templates/docker/src/ui/components/tour/tour-overlay.tsx +298 -0
  369. package/templates/docker/src/ui/components/tour/tour-steps.ts +43 -0
  370. package/templates/docker/src/ui/components/tour/types.ts +21 -0
  371. package/templates/docker/src/ui/components/tour/use-tour.ts +193 -0
  372. package/templates/docker/src/ui/context-reexport.ts +3 -0
  373. package/templates/docker/src/ui/hooks/theme-init-script.ts +17 -0
  374. package/templates/docker/src/ui/hooks/use-admin-fetch.ts +38 -30
  375. package/templates/docker/src/ui/hooks/use-admin-mutation.ts +188 -0
  376. package/templates/docker/src/ui/hooks/use-atlas-transport.ts +225 -0
  377. package/templates/docker/src/ui/hooks/use-branding.ts +68 -0
  378. package/templates/docker/src/ui/hooks/use-conversations.ts +106 -83
  379. package/templates/docker/src/ui/hooks/use-dark-mode.ts +134 -10
  380. package/templates/docker/src/ui/hooks/use-deploy-mode.ts +36 -0
  381. package/templates/docker/src/ui/hooks/use-platform-admin-guard.ts +49 -0
  382. package/templates/docker/src/ui/lib/action-types.ts +11 -63
  383. package/templates/docker/src/ui/lib/admin-schemas.ts +744 -0
  384. package/templates/docker/src/ui/lib/fetch-client.ts +84 -0
  385. package/templates/docker/src/ui/lib/fetch-error.ts +54 -0
  386. package/templates/docker/src/ui/lib/helpers.ts +94 -1
  387. package/templates/docker/src/ui/lib/types.ts +149 -140
  388. package/templates/docker/tsconfig.json +4 -2
  389. package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +17 -14
  390. package/templates/nextjs-standalone/bin/__tests__/failure-threshold.test.ts +148 -0
  391. package/templates/nextjs-standalone/bin/__tests__/fatal-error-propagation.test.ts +267 -0
  392. package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +5 -5
  393. package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +39 -0
  394. package/templates/nextjs-standalone/bin/atlas.ts +981 -1819
  395. package/templates/nextjs-standalone/bin/benchmark.ts +14 -16
  396. package/templates/nextjs-standalone/bin/enrich.ts +7 -2
  397. package/templates/nextjs-standalone/brand.css +13 -0
  398. package/templates/nextjs-standalone/data/cybersec-semantic/catalog.yml +222 -0
  399. package/templates/nextjs-standalone/data/cybersec-semantic/entities/alerts.yml +195 -0
  400. package/templates/nextjs-standalone/data/cybersec-semantic/entities/assets.yml +191 -0
  401. package/templates/nextjs-standalone/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
  402. package/templates/nextjs-standalone/data/cybersec-semantic/entities/incidents.yml +219 -0
  403. package/templates/nextjs-standalone/data/cybersec-semantic/entities/organizations.yml +136 -0
  404. package/templates/nextjs-standalone/data/cybersec-semantic/entities/plans.yml +114 -0
  405. package/templates/nextjs-standalone/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
  406. package/templates/nextjs-standalone/data/cybersec-semantic/entities/scan_results.yml +215 -0
  407. package/templates/nextjs-standalone/data/cybersec-semantic/entities/scans.yml +180 -0
  408. package/templates/nextjs-standalone/data/cybersec-semantic/entities/subscriptions.yml +184 -0
  409. package/templates/nextjs-standalone/data/cybersec-semantic/entities/users.yml +140 -0
  410. package/templates/nextjs-standalone/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
  411. package/templates/nextjs-standalone/data/cybersec-semantic/glossary.yml +207 -0
  412. package/templates/nextjs-standalone/data/cybersec-semantic/metrics/business.yml +148 -0
  413. package/templates/nextjs-standalone/data/cybersec-semantic/metrics/compliance.yml +138 -0
  414. package/templates/nextjs-standalone/data/cybersec-semantic/metrics/security.yml +181 -0
  415. package/templates/nextjs-standalone/data/cybersec.sql +8 -8
  416. package/templates/nextjs-standalone/data/demo.sql +3 -0
  417. package/templates/nextjs-standalone/data/ecommerce-semantic/catalog.yml +221 -0
  418. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/categories.yml +91 -0
  419. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/customers.yml +133 -0
  420. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
  421. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
  422. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/order_items.yml +159 -0
  423. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/orders.yml +199 -0
  424. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/payments.yml +140 -0
  425. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
  426. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/products.yml +178 -0
  427. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/promotions.yml +171 -0
  428. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/returns.yml +144 -0
  429. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/sellers.yml +124 -0
  430. package/templates/nextjs-standalone/data/ecommerce-semantic/entities/shipments.yml +159 -0
  431. package/templates/nextjs-standalone/data/ecommerce-semantic/glossary.yml +193 -0
  432. package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/customers.yml +116 -0
  433. package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/operations.yml +131 -0
  434. package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/revenue.yml +120 -0
  435. package/templates/nextjs-standalone/docs/deploy.md +2 -1
  436. package/templates/nextjs-standalone/ee/src/__mocks__/internal.ts +170 -0
  437. package/templates/nextjs-standalone/ee/src/audit/purge-scheduler.ts +113 -0
  438. package/templates/nextjs-standalone/ee/src/audit/retention.ts +467 -0
  439. package/templates/nextjs-standalone/ee/src/auth/ip-allowlist.ts +367 -0
  440. package/templates/nextjs-standalone/ee/src/auth/roles.ts +562 -0
  441. package/templates/nextjs-standalone/ee/src/auth/scim.ts +343 -0
  442. package/templates/nextjs-standalone/ee/src/auth/sso.ts +538 -0
  443. package/templates/nextjs-standalone/ee/src/backups/engine.ts +355 -0
  444. package/templates/nextjs-standalone/ee/src/backups/index.ts +26 -0
  445. package/templates/nextjs-standalone/ee/src/backups/restore.ts +169 -0
  446. package/templates/nextjs-standalone/ee/src/backups/scheduler.ts +153 -0
  447. package/templates/nextjs-standalone/ee/src/backups/verify.ts +124 -0
  448. package/templates/nextjs-standalone/ee/src/branding/white-label.ts +228 -0
  449. package/templates/nextjs-standalone/ee/src/compliance/masking.ts +477 -0
  450. package/templates/nextjs-standalone/ee/src/compliance/patterns.ts +16 -0
  451. package/templates/nextjs-standalone/ee/src/compliance/pii-detection.ts +217 -0
  452. package/templates/nextjs-standalone/ee/src/compliance/reports.ts +402 -0
  453. package/templates/nextjs-standalone/ee/src/deploy-mode.ts +37 -0
  454. package/templates/nextjs-standalone/ee/src/governance/approval.ts +699 -0
  455. package/templates/nextjs-standalone/ee/src/index.ts +74 -0
  456. package/templates/nextjs-standalone/ee/src/platform/domains.ts +562 -0
  457. package/templates/nextjs-standalone/ee/src/platform/model-routing.ts +382 -0
  458. package/templates/nextjs-standalone/ee/src/platform/residency.ts +265 -0
  459. package/templates/nextjs-standalone/ee/src/sla/alerting.ts +382 -0
  460. package/templates/nextjs-standalone/ee/src/sla/index.ts +12 -0
  461. package/templates/nextjs-standalone/ee/src/sla/metrics.ts +275 -0
  462. package/templates/nextjs-standalone/ee/src/test-setup.ts +1 -0
  463. package/templates/nextjs-standalone/next.config.ts +1 -1
  464. package/templates/nextjs-standalone/package.json +50 -30
  465. package/templates/nextjs-standalone/src/api/index.ts +336 -24
  466. package/templates/nextjs-standalone/src/api/routes/actions.ts +443 -176
  467. package/templates/nextjs-standalone/src/api/routes/admin-abuse.ts +219 -0
  468. package/templates/nextjs-standalone/src/api/routes/admin-approval.ts +418 -0
  469. package/templates/nextjs-standalone/src/api/routes/admin-audit-retention.ts +405 -0
  470. package/templates/nextjs-standalone/src/api/routes/admin-auth.ts +122 -0
  471. package/templates/nextjs-standalone/src/api/routes/admin-branding.ts +252 -0
  472. package/templates/nextjs-standalone/src/api/routes/admin-compliance.ts +352 -0
  473. package/templates/nextjs-standalone/src/api/routes/admin-domains.ts +334 -0
  474. package/templates/nextjs-standalone/src/api/routes/admin-integrations.ts +2667 -0
  475. package/templates/nextjs-standalone/src/api/routes/admin-ip-allowlist.ts +261 -0
  476. package/templates/nextjs-standalone/src/api/routes/admin-learned-patterns.ts +525 -0
  477. package/templates/nextjs-standalone/src/api/routes/admin-model-config.ts +252 -0
  478. package/templates/nextjs-standalone/src/api/routes/admin-onboarding-emails.ts +145 -0
  479. package/templates/nextjs-standalone/src/api/routes/admin-orgs.ts +710 -0
  480. package/templates/nextjs-standalone/src/api/routes/admin-prompts.ts +694 -0
  481. package/templates/nextjs-standalone/src/api/routes/admin-residency.ts +570 -0
  482. package/templates/nextjs-standalone/src/api/routes/admin-roles.ts +296 -0
  483. package/templates/nextjs-standalone/src/api/routes/admin-router.ts +120 -0
  484. package/templates/nextjs-standalone/src/api/routes/admin-sandbox.ts +417 -0
  485. package/templates/nextjs-standalone/src/api/routes/admin-scim.ts +262 -0
  486. package/templates/nextjs-standalone/src/api/routes/admin-sso.ts +545 -0
  487. package/templates/nextjs-standalone/src/api/routes/admin-suggestions.ts +176 -0
  488. package/templates/nextjs-standalone/src/api/routes/admin-usage.ts +310 -0
  489. package/templates/nextjs-standalone/src/api/routes/admin.ts +4156 -898
  490. package/templates/nextjs-standalone/src/api/routes/auth-preamble.ts +105 -0
  491. package/templates/nextjs-standalone/src/api/routes/billing.ts +397 -0
  492. package/templates/nextjs-standalone/src/api/routes/chat.ts +597 -334
  493. package/templates/nextjs-standalone/src/api/routes/conversations.ts +987 -132
  494. package/templates/nextjs-standalone/src/api/routes/demo.ts +673 -0
  495. package/templates/nextjs-standalone/src/api/routes/discord.ts +274 -0
  496. package/templates/nextjs-standalone/src/api/routes/ee-error-handler.ts +32 -0
  497. package/templates/nextjs-standalone/src/api/routes/health.ts +129 -14
  498. package/templates/nextjs-standalone/src/api/routes/middleware.ts +244 -0
  499. package/templates/nextjs-standalone/src/api/routes/onboarding-emails.ts +134 -0
  500. package/templates/nextjs-standalone/src/api/routes/onboarding.ts +1109 -0
  501. package/templates/nextjs-standalone/src/api/routes/openapi.ts +184 -1597
  502. package/templates/nextjs-standalone/src/api/routes/platform-admin.ts +760 -0
  503. package/templates/nextjs-standalone/src/api/routes/platform-backups.ts +436 -0
  504. package/templates/nextjs-standalone/src/api/routes/platform-domains.ts +235 -0
  505. package/templates/nextjs-standalone/src/api/routes/platform-residency.ts +257 -0
  506. package/templates/nextjs-standalone/src/api/routes/platform-sla.ts +379 -0
  507. package/templates/nextjs-standalone/src/api/routes/prompts.ts +221 -0
  508. package/templates/nextjs-standalone/src/api/routes/public-branding.ts +106 -0
  509. package/templates/nextjs-standalone/src/api/routes/query.ts +330 -219
  510. package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +393 -297
  511. package/templates/nextjs-standalone/src/api/routes/semantic.ts +179 -0
  512. package/templates/nextjs-standalone/src/api/routes/sessions.ts +210 -0
  513. package/templates/nextjs-standalone/src/api/routes/shared-domains.ts +98 -0
  514. package/templates/nextjs-standalone/src/api/routes/shared-schemas.ts +139 -0
  515. package/templates/nextjs-standalone/src/api/routes/slack.ts +209 -52
  516. package/templates/nextjs-standalone/src/api/routes/suggestions.ts +233 -0
  517. package/templates/nextjs-standalone/src/api/routes/tables.ts +67 -0
  518. package/templates/nextjs-standalone/src/api/routes/teams.ts +222 -0
  519. package/templates/nextjs-standalone/src/api/routes/validate-sql.ts +188 -0
  520. package/templates/nextjs-standalone/src/api/routes/validation-hook.ts +62 -0
  521. package/templates/nextjs-standalone/src/api/routes/widget-loader.ts +356 -0
  522. package/templates/nextjs-standalone/src/api/routes/widget.ts +428 -0
  523. package/templates/nextjs-standalone/src/api/routes/wizard.ts +852 -0
  524. package/templates/nextjs-standalone/src/api/server.ts +187 -69
  525. package/templates/nextjs-standalone/src/app/error.tsx +5 -2
  526. package/templates/nextjs-standalone/src/app/globals.css +1 -1
  527. package/templates/nextjs-standalone/src/app/layout.tsx +7 -2
  528. package/templates/nextjs-standalone/src/app/page.tsx +39 -5
  529. package/templates/nextjs-standalone/src/components/data-table/data-table-column-header.tsx +99 -0
  530. package/templates/nextjs-standalone/src/components/data-table/data-table-date-filter.tsx +225 -0
  531. package/templates/nextjs-standalone/src/components/data-table/data-table-expandable.tsx +125 -0
  532. package/templates/nextjs-standalone/src/components/data-table/data-table-faceted-filter.tsx +189 -0
  533. package/templates/nextjs-standalone/src/components/data-table/data-table-pagination.tsx +112 -0
  534. package/templates/nextjs-standalone/src/components/data-table/data-table-range-filter.tsx +122 -0
  535. package/templates/nextjs-standalone/src/components/data-table/data-table-slider-filter.tsx +256 -0
  536. package/templates/nextjs-standalone/src/components/data-table/data-table-sort-list.tsx +407 -0
  537. package/templates/nextjs-standalone/src/components/data-table/data-table-toolbar.tsx +149 -0
  538. package/templates/nextjs-standalone/src/components/data-table/data-table-view-options.tsx +89 -0
  539. package/templates/nextjs-standalone/src/components/data-table/data-table.tsx +105 -0
  540. package/templates/nextjs-standalone/src/components/form-dialog.tsx +135 -0
  541. package/templates/nextjs-standalone/src/components/ui/accordion.tsx +66 -0
  542. package/templates/nextjs-standalone/src/components/ui/calendar.tsx +220 -0
  543. package/templates/nextjs-standalone/src/components/ui/checkbox.tsx +32 -0
  544. package/templates/nextjs-standalone/src/components/ui/faceted.tsx +283 -0
  545. package/templates/nextjs-standalone/src/components/ui/form.tsx +167 -0
  546. package/templates/nextjs-standalone/src/components/ui/label.tsx +24 -0
  547. package/templates/nextjs-standalone/src/components/ui/popover.tsx +89 -0
  548. package/templates/nextjs-standalone/src/components/ui/progress.tsx +31 -0
  549. package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +6 -2
  550. package/templates/nextjs-standalone/src/components/ui/slider.tsx +63 -0
  551. package/templates/nextjs-standalone/src/components/ui/sortable.tsx +581 -0
  552. package/templates/nextjs-standalone/src/components/ui/switch.tsx +35 -0
  553. package/templates/nextjs-standalone/src/components/ui/textarea.tsx +18 -0
  554. package/templates/nextjs-standalone/src/config/data-table.ts +82 -0
  555. package/templates/nextjs-standalone/src/env-check.ts +74 -0
  556. package/templates/nextjs-standalone/src/hooks/use-callback-ref.ts +27 -0
  557. package/templates/nextjs-standalone/src/hooks/use-data-table.ts +316 -0
  558. package/templates/nextjs-standalone/src/hooks/use-debounced-callback.ts +28 -0
  559. package/templates/nextjs-standalone/src/lib/action-types.ts +7 -41
  560. package/templates/nextjs-standalone/src/lib/agent-query.ts +4 -2
  561. package/templates/nextjs-standalone/src/lib/agent.ts +363 -31
  562. package/templates/nextjs-standalone/src/lib/api-url.ts +2 -3
  563. package/templates/nextjs-standalone/src/lib/auth/admin-permissions.ts +38 -0
  564. package/templates/nextjs-standalone/src/lib/auth/audit.ts +19 -4
  565. package/templates/nextjs-standalone/src/lib/auth/byot.ts +3 -3
  566. package/templates/nextjs-standalone/src/lib/auth/detect.ts +29 -8
  567. package/templates/nextjs-standalone/src/lib/auth/managed.ts +104 -14
  568. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +53 -6
  569. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +140 -15
  570. package/templates/nextjs-standalone/src/lib/auth/oauth-state.ts +123 -0
  571. package/templates/nextjs-standalone/src/lib/auth/org-permissions.ts +55 -0
  572. package/templates/nextjs-standalone/src/lib/auth/permissions.ts +26 -19
  573. package/templates/nextjs-standalone/src/lib/auth/server.ts +355 -9
  574. package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +3 -3
  575. package/templates/nextjs-standalone/src/lib/auth/types.ts +15 -21
  576. package/templates/nextjs-standalone/src/lib/billing/enforcement.ts +368 -0
  577. package/templates/nextjs-standalone/src/lib/billing/plans.ts +155 -0
  578. package/templates/nextjs-standalone/src/lib/cache/index.ts +92 -0
  579. package/templates/nextjs-standalone/src/lib/cache/keys.ts +30 -0
  580. package/templates/nextjs-standalone/src/lib/cache/lru.ts +79 -0
  581. package/templates/nextjs-standalone/src/lib/cache/types.ts +31 -0
  582. package/templates/nextjs-standalone/src/lib/compose-refs.ts +62 -0
  583. package/templates/nextjs-standalone/src/lib/config.ts +563 -11
  584. package/templates/nextjs-standalone/src/lib/connection-types.ts +9 -0
  585. package/templates/nextjs-standalone/src/lib/conversation-types.ts +1 -25
  586. package/templates/nextjs-standalone/src/lib/conversations.ts +345 -14
  587. package/templates/nextjs-standalone/src/lib/data-table.ts +61 -0
  588. package/templates/nextjs-standalone/src/lib/db/connection.ts +793 -39
  589. package/templates/nextjs-standalone/src/lib/db/internal.ts +985 -139
  590. package/templates/nextjs-standalone/src/lib/db/migrate.ts +295 -0
  591. package/templates/nextjs-standalone/src/lib/db/migrations/0000_baseline.sql +703 -0
  592. package/templates/nextjs-standalone/src/lib/db/migrations/0001_teams_installations.sql +14 -0
  593. package/templates/nextjs-standalone/src/lib/db/migrations/0002_discord_installations.sql +14 -0
  594. package/templates/nextjs-standalone/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
  595. package/templates/nextjs-standalone/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
  596. package/templates/nextjs-standalone/src/lib/db/migrations/0005_oauth_state.sql +16 -0
  597. package/templates/nextjs-standalone/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
  598. package/templates/nextjs-standalone/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
  599. package/templates/nextjs-standalone/src/lib/db/migrations/0008_github_installations.sql +14 -0
  600. package/templates/nextjs-standalone/src/lib/db/migrations/0009_linear_installations.sql +15 -0
  601. package/templates/nextjs-standalone/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
  602. package/templates/nextjs-standalone/src/lib/db/migrations/0011_email_installations.sql +16 -0
  603. package/templates/nextjs-standalone/src/lib/db/migrations/0012_region_migrations.sql +25 -0
  604. package/templates/nextjs-standalone/src/lib/db/schema.ts +1120 -0
  605. package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +89 -139
  606. package/templates/nextjs-standalone/src/lib/demo.ts +308 -0
  607. package/templates/nextjs-standalone/src/lib/discord/store.ts +225 -0
  608. package/templates/nextjs-standalone/src/lib/effect/ai.ts +243 -0
  609. package/templates/nextjs-standalone/src/lib/effect/errors.ts +234 -0
  610. package/templates/nextjs-standalone/src/lib/effect/hono.ts +454 -0
  611. package/templates/nextjs-standalone/src/lib/effect/index.ts +137 -0
  612. package/templates/nextjs-standalone/src/lib/effect/layers.ts +496 -0
  613. package/templates/nextjs-standalone/src/lib/effect/services.ts +776 -0
  614. package/templates/nextjs-standalone/src/lib/effect/sql.ts +178 -0
  615. package/templates/nextjs-standalone/src/lib/effect/toolkit.ts +123 -0
  616. package/templates/nextjs-standalone/src/lib/email/delivery.ts +232 -0
  617. package/templates/nextjs-standalone/src/lib/email/engine.ts +349 -0
  618. package/templates/nextjs-standalone/src/lib/email/hooks.ts +107 -0
  619. package/templates/nextjs-standalone/src/lib/email/index.ts +16 -0
  620. package/templates/nextjs-standalone/src/lib/email/scheduler.ts +72 -0
  621. package/templates/nextjs-standalone/src/lib/email/sequence.ts +73 -0
  622. package/templates/nextjs-standalone/src/lib/email/store.ts +163 -0
  623. package/templates/nextjs-standalone/src/lib/email/templates.ts +215 -0
  624. package/templates/nextjs-standalone/src/lib/format.test.ts +117 -0
  625. package/templates/nextjs-standalone/src/lib/format.ts +67 -0
  626. package/templates/nextjs-standalone/src/lib/gchat/store.ts +202 -0
  627. package/templates/nextjs-standalone/src/lib/github/store.ts +197 -0
  628. package/templates/nextjs-standalone/src/lib/id.ts +29 -0
  629. package/templates/nextjs-standalone/src/lib/integrations/types.ts +166 -0
  630. package/templates/nextjs-standalone/src/lib/learn/pattern-analyzer.ts +224 -0
  631. package/templates/nextjs-standalone/src/lib/learn/pattern-cache.ts +229 -0
  632. package/templates/nextjs-standalone/src/lib/learn/pattern-proposer.ts +87 -0
  633. package/templates/nextjs-standalone/src/lib/learn/suggestion-helpers.ts +34 -0
  634. package/templates/nextjs-standalone/src/lib/learn/suggestions.ts +139 -0
  635. package/templates/nextjs-standalone/src/lib/linear/store.ts +200 -0
  636. package/templates/nextjs-standalone/src/lib/logger.ts +35 -3
  637. package/templates/nextjs-standalone/src/lib/metering.ts +272 -0
  638. package/templates/nextjs-standalone/src/lib/parsers.ts +99 -0
  639. package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +13 -11
  640. package/templates/nextjs-standalone/src/lib/plugins/index.ts +3 -1
  641. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +58 -6
  642. package/templates/nextjs-standalone/src/lib/plugins/settings.ts +147 -0
  643. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +6 -9
  644. package/templates/nextjs-standalone/src/lib/profiler.ts +1665 -0
  645. package/templates/nextjs-standalone/src/lib/providers.ts +188 -13
  646. package/templates/nextjs-standalone/src/lib/rls.ts +172 -60
  647. package/templates/nextjs-standalone/src/lib/sandbox/credentials.ts +206 -0
  648. package/templates/nextjs-standalone/src/lib/sandbox/validate.ts +179 -0
  649. package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +26 -94
  650. package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +174 -34
  651. package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -150
  652. package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +190 -154
  653. package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +74 -23
  654. package/templates/nextjs-standalone/src/lib/scheduler/preview.ts +72 -0
  655. package/templates/nextjs-standalone/src/lib/security/abuse.ts +463 -0
  656. package/templates/nextjs-standalone/src/lib/semantic/diff.ts +267 -0
  657. package/templates/nextjs-standalone/src/lib/semantic/entities.ts +167 -0
  658. package/templates/nextjs-standalone/src/lib/semantic/files.ts +283 -0
  659. package/templates/nextjs-standalone/src/lib/semantic/index.ts +27 -0
  660. package/templates/nextjs-standalone/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
  661. package/templates/nextjs-standalone/src/lib/semantic/sync.ts +581 -0
  662. package/templates/nextjs-standalone/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
  663. package/templates/nextjs-standalone/src/lib/settings.ts +817 -0
  664. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +13 -0
  665. package/templates/nextjs-standalone/src/lib/slack/store.ts +134 -25
  666. package/templates/nextjs-standalone/src/lib/startup.ts +528 -362
  667. package/templates/nextjs-standalone/src/lib/teams/store.ts +216 -0
  668. package/templates/nextjs-standalone/src/lib/telegram/store.ts +202 -0
  669. package/templates/nextjs-standalone/src/lib/telemetry.ts +40 -0
  670. package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +8 -5
  671. package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +3 -1
  672. package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +276 -93
  673. package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +2 -2
  674. package/templates/nextjs-standalone/src/lib/tools/backends/detect.ts +16 -0
  675. package/templates/nextjs-standalone/src/lib/tools/backends/index.ts +11 -0
  676. package/templates/nextjs-standalone/src/lib/tools/backends/nsjail.ts +213 -0
  677. package/templates/nextjs-standalone/src/lib/tools/backends/shared.ts +103 -0
  678. package/templates/nextjs-standalone/src/lib/tools/backends/types.ts +26 -0
  679. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +7 -228
  680. package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +4 -29
  681. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +18 -2
  682. package/templates/nextjs-standalone/src/lib/tools/explore.ts +246 -54
  683. package/templates/nextjs-standalone/src/lib/tools/index.ts +17 -0
  684. package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +11 -139
  685. package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +9 -132
  686. package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +184 -3
  687. package/templates/nextjs-standalone/src/lib/tools/python-stream.ts +33 -0
  688. package/templates/nextjs-standalone/src/lib/tools/python-wrapper.ts +129 -0
  689. package/templates/nextjs-standalone/src/lib/tools/python.ts +115 -15
  690. package/templates/nextjs-standalone/src/lib/tools/registry.ts +14 -2
  691. package/templates/nextjs-standalone/src/lib/tools/sql.ts +778 -362
  692. package/templates/nextjs-standalone/src/lib/tracing.ts +16 -0
  693. package/templates/nextjs-standalone/src/lib/whatsapp/store.ts +198 -0
  694. package/templates/nextjs-standalone/src/lib/workspace.ts +89 -0
  695. package/templates/nextjs-standalone/src/progress.ts +121 -0
  696. package/templates/nextjs-standalone/src/types/data-table.ts +48 -0
  697. package/templates/nextjs-standalone/src/ui/atlas-chat-reexport.ts +3 -0
  698. package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +26 -19
  699. package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +3 -3
  700. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +57 -39
  701. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +213 -35
  702. package/templates/nextjs-standalone/src/ui/components/admin/delivery-status-badge.tsx +53 -0
  703. package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +27 -6
  704. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -52
  705. package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +2 -2
  706. package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +28 -5
  707. package/templates/nextjs-standalone/src/ui/components/admin-content-wrapper.tsx +87 -0
  708. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +449 -166
  709. package/templates/nextjs-standalone/src/ui/components/branding-head.tsx +41 -0
  710. package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +62 -5
  711. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +316 -125
  712. package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +4 -4
  713. package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +45 -4
  714. package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +86 -5
  715. package/templates/nextjs-standalone/src/ui/components/chat/follow-up-chips.tsx +29 -0
  716. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +24 -0
  717. package/templates/nextjs-standalone/src/ui/components/chat/prompt-library.tsx +206 -0
  718. package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +106 -78
  719. package/templates/nextjs-standalone/src/ui/components/chat/result-card-base.tsx +101 -0
  720. package/templates/nextjs-standalone/src/ui/components/chat/share-dialog.tsx +377 -0
  721. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +94 -73
  722. package/templates/nextjs-standalone/src/ui/components/chat/suggestion-chips.tsx +46 -0
  723. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +16 -4
  724. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +48 -17
  725. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +38 -24
  726. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
  727. package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +9 -2
  728. package/templates/nextjs-standalone/src/ui/components/error-boundary.tsx +66 -0
  729. package/templates/nextjs-standalone/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
  730. package/templates/nextjs-standalone/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
  731. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
  732. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
  733. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
  734. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell.tsx +119 -0
  735. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
  736. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-export.ts +287 -0
  737. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
  738. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-shell.tsx +266 -0
  739. package/templates/nextjs-standalone/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
  740. package/templates/nextjs-standalone/src/ui/components/notebook/types.ts +39 -0
  741. package/templates/nextjs-standalone/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
  742. package/templates/nextjs-standalone/src/ui/components/notebook/use-notebook.ts +684 -0
  743. package/templates/nextjs-standalone/src/ui/components/org-switcher.tsx +111 -0
  744. package/templates/nextjs-standalone/src/ui/components/region-picker.tsx +103 -0
  745. package/templates/nextjs-standalone/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
  746. package/templates/nextjs-standalone/src/ui/components/social-icons.tsx +26 -0
  747. package/templates/nextjs-standalone/src/ui/components/tour/guided-tour.tsx +81 -0
  748. package/templates/nextjs-standalone/src/ui/components/tour/index.ts +5 -0
  749. package/templates/nextjs-standalone/src/ui/components/tour/nav-bar.tsx +100 -0
  750. package/templates/nextjs-standalone/src/ui/components/tour/tour-overlay.tsx +298 -0
  751. package/templates/nextjs-standalone/src/ui/components/tour/tour-steps.ts +43 -0
  752. package/templates/nextjs-standalone/src/ui/components/tour/types.ts +21 -0
  753. package/templates/nextjs-standalone/src/ui/components/tour/use-tour.ts +193 -0
  754. package/templates/nextjs-standalone/src/ui/context-reexport.ts +3 -0
  755. package/templates/nextjs-standalone/src/ui/hooks/theme-init-script.ts +17 -0
  756. package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +38 -30
  757. package/templates/nextjs-standalone/src/ui/hooks/use-admin-mutation.ts +188 -0
  758. package/templates/nextjs-standalone/src/ui/hooks/use-atlas-transport.ts +225 -0
  759. package/templates/nextjs-standalone/src/ui/hooks/use-branding.ts +68 -0
  760. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +106 -83
  761. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +134 -10
  762. package/templates/nextjs-standalone/src/ui/hooks/use-deploy-mode.ts +36 -0
  763. package/templates/nextjs-standalone/src/ui/hooks/use-platform-admin-guard.ts +49 -0
  764. package/templates/nextjs-standalone/src/ui/lib/action-types.ts +11 -63
  765. package/templates/nextjs-standalone/src/ui/lib/admin-schemas.ts +744 -0
  766. package/templates/nextjs-standalone/src/ui/lib/fetch-client.ts +84 -0
  767. package/templates/nextjs-standalone/src/ui/lib/fetch-error.ts +54 -0
  768. package/templates/nextjs-standalone/src/ui/lib/helpers.ts +94 -1
  769. package/templates/nextjs-standalone/src/ui/lib/types.ts +149 -140
  770. package/templates/nextjs-standalone/tsconfig.json +3 -2
  771. package/templates/docker/src/api/__tests__/actions.test.ts +0 -683
  772. package/templates/docker/src/api/__tests__/admin.test.ts +0 -820
  773. package/templates/docker/src/api/__tests__/auth.test.ts +0 -165
  774. package/templates/docker/src/api/__tests__/chat.test.ts +0 -376
  775. package/templates/docker/src/api/__tests__/conversations.test.ts +0 -555
  776. package/templates/docker/src/api/__tests__/cors.test.ts +0 -135
  777. package/templates/docker/src/api/__tests__/health-plugin.test.ts +0 -176
  778. package/templates/docker/src/api/__tests__/health.test.ts +0 -283
  779. package/templates/docker/src/api/__tests__/query.test.ts +0 -891
  780. package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +0 -601
  781. package/templates/docker/src/api/__tests__/slack.test.ts +0 -847
  782. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +0 -439
  783. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +0 -131
  784. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
  785. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +0 -516
  786. package/templates/docker/src/lib/__tests__/config-actions.test.ts +0 -166
  787. package/templates/docker/src/lib/__tests__/config.test.ts +0 -1113
  788. package/templates/docker/src/lib/__tests__/conversations.test.ts +0 -589
  789. package/templates/docker/src/lib/__tests__/errors.test.ts +0 -256
  790. package/templates/docker/src/lib/__tests__/logger.test.ts +0 -200
  791. package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
  792. package/templates/docker/src/lib/__tests__/providers.test.ts +0 -130
  793. package/templates/docker/src/lib/__tests__/rls.test.ts +0 -435
  794. package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
  795. package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
  796. package/templates/docker/src/lib/__tests__/semantic-index.test.ts +0 -547
  797. package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +0 -544
  798. package/templates/docker/src/lib/__tests__/semantic.test.ts +0 -363
  799. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +0 -461
  800. package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +0 -429
  801. package/templates/docker/src/lib/__tests__/startup.test.ts +0 -470
  802. package/templates/docker/src/lib/__tests__/tracing.test.ts +0 -28
  803. package/templates/docker/src/lib/auth/__tests__/audit.test.ts +0 -418
  804. package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
  805. package/templates/docker/src/lib/auth/__tests__/byot.test.ts +0 -366
  806. package/templates/docker/src/lib/auth/__tests__/detect.test.ts +0 -190
  807. package/templates/docker/src/lib/auth/__tests__/managed.test.ts +0 -173
  808. package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +0 -456
  809. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +0 -203
  810. package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +0 -225
  811. package/templates/docker/src/lib/auth/__tests__/server.test.ts +0 -34
  812. package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +0 -176
  813. package/templates/docker/src/lib/auth/__tests__/types.test.ts +0 -44
  814. package/templates/docker/src/lib/db/__tests__/connection.test.ts +0 -144
  815. package/templates/docker/src/lib/db/__tests__/internal.test.ts +0 -387
  816. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +0 -190
  817. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
  818. package/templates/docker/src/lib/db/__tests__/registry.test.ts +0 -398
  819. package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
  820. package/templates/docker/src/lib/errors.ts +0 -154
  821. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
  822. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +0 -529
  823. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +0 -875
  824. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +0 -373
  825. package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +0 -49
  826. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +0 -799
  827. package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
  828. package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +0 -248
  829. package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
  830. package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
  831. package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
  832. package/templates/docker/src/lib/scheduler/index.ts +0 -7
  833. package/templates/docker/src/lib/slack/__tests__/api.test.ts +0 -160
  834. package/templates/docker/src/lib/slack/__tests__/format.test.ts +0 -237
  835. package/templates/docker/src/lib/slack/__tests__/store.test.ts +0 -188
  836. package/templates/docker/src/lib/slack/__tests__/threads.test.ts +0 -112
  837. package/templates/docker/src/lib/slack/__tests__/verify.test.ts +0 -111
  838. package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
  839. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
  840. package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
  841. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
  842. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
  843. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
  844. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
  845. package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
  846. package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
  847. package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
  848. package/templates/docker/src/lib/tools/__tests__/python.test.ts +0 -331
  849. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
  850. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +0 -242
  851. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
  852. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
  853. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
  854. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +0 -709
  855. package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
  856. package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +0 -378
  857. package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
  858. package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
  859. package/templates/docker/src/test-setup.ts +0 -38
  860. package/templates/docker/src/types/vercel-sandbox.d.ts +0 -61
  861. package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +0 -116
  862. package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +0 -683
  863. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +0 -820
  864. package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +0 -165
  865. package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +0 -376
  866. package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +0 -555
  867. package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +0 -135
  868. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +0 -176
  869. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +0 -283
  870. package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +0 -891
  871. package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +0 -601
  872. package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +0 -847
  873. package/templates/nextjs-standalone/src/app/global-error.tsx +0 -68
  874. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +0 -439
  875. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +0 -131
  876. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
  877. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +0 -516
  878. package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +0 -166
  879. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +0 -1113
  880. package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +0 -589
  881. package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +0 -256
  882. package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +0 -200
  883. package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
  884. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +0 -130
  885. package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +0 -435
  886. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
  887. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
  888. package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +0 -547
  889. package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +0 -544
  890. package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +0 -363
  891. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +0 -461
  892. package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +0 -429
  893. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +0 -470
  894. package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +0 -28
  895. package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +0 -418
  896. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
  897. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +0 -366
  898. package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +0 -190
  899. package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +0 -173
  900. package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +0 -456
  901. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +0 -203
  902. package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +0 -225
  903. package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +0 -34
  904. package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +0 -176
  905. package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +0 -44
  906. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +0 -144
  907. package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +0 -387
  908. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +0 -190
  909. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
  910. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +0 -398
  911. package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
  912. package/templates/nextjs-standalone/src/lib/errors.ts +0 -154
  913. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
  914. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +0 -529
  915. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +0 -875
  916. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +0 -373
  917. package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +0 -49
  918. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +0 -799
  919. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
  920. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +0 -248
  921. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
  922. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
  923. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
  924. package/templates/nextjs-standalone/src/lib/scheduler/index.ts +0 -7
  925. package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +0 -160
  926. package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +0 -237
  927. package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +0 -188
  928. package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +0 -112
  929. package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +0 -111
  930. package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
  931. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
  932. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
  933. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
  934. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
  935. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
  936. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
  937. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
  938. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
  939. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
  940. package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +0 -331
  941. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
  942. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +0 -242
  943. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
  944. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
  945. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
  946. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +0 -709
  947. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
  948. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +0 -378
  949. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
  950. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
  951. package/templates/nextjs-standalone/src/test-setup.ts +0 -38
  952. package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +0 -116
@@ -36,7 +36,7 @@
36
36
  * tables and views are profiled automatically.
37
37
  *
38
38
  * Requires ATLAS_DATASOURCE_URL in environment.
39
- * Supports PostgreSQL (postgresql://...) and MySQL (mysql://...).
39
+ * Supports PostgreSQL, MySQL, ClickHouse, Snowflake, DuckDB, and Salesforce.
40
40
  */
41
41
 
42
42
  import { Pool } from "pg";
@@ -46,6 +46,76 @@ import * as yaml from "js-yaml";
46
46
  import * as p from "@clack/prompts";
47
47
  import pc from "picocolors";
48
48
  import { type DBType } from "@atlas/api/lib/db/connection";
49
+ import type { DuckDBConnection } from "@duckdb/node-api";
50
+ import type { SObjectField } from "../../../plugins/salesforce/src/connection";
51
+ import { checkEnvFile } from "../src/env-check";
52
+ import { type ProfileProgressCallbacks, createProgressTracker, formatDuration } from "../src/progress";
53
+ import {
54
+ type DatabaseObject,
55
+ type ColumnProfile,
56
+ type ForeignKey,
57
+ type TableProfile,
58
+ type ProfileError,
59
+ type ProfilingResult,
60
+ type ProfileLogger,
61
+ isFatalConnectionError,
62
+ checkFailureThreshold,
63
+ isView,
64
+ isMatView,
65
+ isViewLike,
66
+ mapSQLType,
67
+ analyzeTableProfiles,
68
+ generateEntityYAML,
69
+ generateCatalogYAML,
70
+ generateMetricYAML,
71
+ generateGlossaryYAML,
72
+ outputDirForDatasource,
73
+ listPostgresObjects,
74
+ listMySQLObjects,
75
+ profilePostgres,
76
+ profileMySQL,
77
+ } from "@atlas/api/lib/profiler";
78
+
79
+ /** Adapts the profiler's structured logger to CLI console output. */
80
+ const cliProfileLogger: ProfileLogger = {
81
+ info(_obj, msg) { console.log(` ${msg}`); },
82
+ warn(obj, msg) {
83
+ const ctx = [obj.table, obj.column].filter(Boolean).join(".");
84
+ console.warn(` Warning: ${msg}${ctx ? ` (${ctx})` : ""}${obj.err ? `: ${obj.err}` : ""}`);
85
+ },
86
+ error(obj, msg) {
87
+ const ctx = [obj.table, obj.column].filter(Boolean).join(".");
88
+ console.error(` ${msg}${ctx ? ` (${ctx})` : ""}${obj.err ? `: ${obj.err}` : ""}`);
89
+ },
90
+ };
91
+
92
+ // Re-export from shared profiler for test backward compatibility
93
+ export {
94
+ type ColumnProfile,
95
+ type TableProfile,
96
+ type ProfileError,
97
+ type ProfilingResult,
98
+ FATAL_ERROR_PATTERN,
99
+ isFatalConnectionError,
100
+ checkFailureThreshold,
101
+ generateEntityYAML,
102
+ generateCatalogYAML,
103
+ generateMetricYAML,
104
+ generateGlossaryYAML,
105
+ mapSQLType,
106
+ isView,
107
+ isMatView,
108
+ isViewLike,
109
+ entityName,
110
+ outputDirForDatasource,
111
+ inferForeignKeys,
112
+ detectAbandonedTables,
113
+ detectEnumInconsistency,
114
+ detectDenormalizedTables,
115
+ analyzeTableProfiles,
116
+ pluralize,
117
+ singularize,
118
+ } from "@atlas/api/lib/profiler";
49
119
 
50
120
  /** CLI-local DB type detection — supports all URL schemes (core + plugin databases). */
51
121
  function detectDBType(url: string): DBType {
@@ -70,67 +140,19 @@ async function loadDuckDB() {
70
140
  const SEMANTIC_DIR = path.resolve("semantic");
71
141
  const ENTITIES_DIR = path.join(SEMANTIC_DIR, "entities");
72
142
 
73
- // --- Interfaces ---
74
-
75
- export type ObjectType = "table" | "view" | "materialized_view";
76
-
77
- export interface DatabaseObject {
78
- name: string;
79
- type: ObjectType;
80
- }
81
-
82
- export interface ColumnProfile {
83
- name: string;
84
- type: string;
85
- nullable: boolean;
86
- unique_count: number | null;
87
- null_count: number | null;
88
- sample_values: string[];
89
- is_primary_key: boolean;
90
- is_foreign_key: boolean;
91
- fk_target_table: string | null;
92
- fk_target_column: string | null;
93
- is_enum_like: boolean;
94
- profiler_notes: string[];
95
- }
96
-
97
- export interface ForeignKey {
98
- from_column: string;
99
- to_table: string;
100
- to_column: string;
101
- source: "constraint" | "inferred";
102
- }
103
-
104
- export interface TableProfile {
105
- table_name: string;
106
- object_type: ObjectType;
107
- row_count: number;
108
- columns: ColumnProfile[];
109
- primary_key_columns: string[];
110
- foreign_keys: ForeignKey[];
111
- inferred_foreign_keys: ForeignKey[];
112
- profiler_notes: string[];
113
- table_flags: {
114
- possibly_abandoned: boolean;
115
- possibly_denormalized: boolean;
116
- };
117
- matview_populated?: boolean;
118
- partition_info?: { strategy: "range" | "list" | "hash"; key: string; children: string[] };
119
- }
120
-
121
- /** Check whether a profile represents a database view. */
122
- export function isView(profile: TableProfile): boolean {
123
- return profile.object_type === "view";
124
- }
125
-
126
- /** Check whether a profile represents a materialized view. */
127
- export function isMatView(profile: TableProfile): boolean {
128
- return profile.object_type === "materialized_view";
129
- }
130
-
131
- /** Check whether a profile is view-like (view or materialized view) — skip PK/FK/measures/patterns. */
132
- export function isViewLike(profile: TableProfile): boolean {
133
- return profile.object_type === "view" || profile.object_type === "materialized_view";
143
+ /** Log a warning summary for profiling errors (first 5 + overflow). CLI-specific: uses console.warn formatting rather than the profiler's structured logger. */
144
+ export function logProfilingErrors(errors: ProfileError[], total: number): void {
145
+ const pct = Math.round((errors.length / total) * 100);
146
+ console.warn(
147
+ `\nWarning: ${errors.length}/${total} tables (${pct}%) failed to profile:`
148
+ );
149
+ const preview = errors.slice(0, 5);
150
+ for (const e of preview) {
151
+ console.warn(` - ${e.table}: ${e.error}`);
152
+ }
153
+ if (errors.length > 5) {
154
+ console.warn(` ... and ${errors.length - 5} more`);
155
+ }
134
156
  }
135
157
 
136
158
  // --- Shared helpers ---
@@ -163,638 +185,11 @@ function requireFlagIdentifier(args: string[], flag: string, label: string): str
163
185
  return value;
164
186
  }
165
187
 
166
- // --- PostgreSQL profiler ---
167
-
168
- /** Schema-qualified table reference for SQL queries. */
169
- function pgTableRef(tableName: string, schema: string): string {
170
- const safeTable = tableName.replace(/"/g, '""');
171
- const safeSchema = schema.replace(/"/g, '""');
172
- return schema === "public" ? `"${safeTable}"` : `"${safeSchema}"."${safeTable}"`;
173
- }
174
-
175
- async function queryPrimaryKeys(
176
- pool: Pool,
177
- tableName: string,
178
- schema: string = "public"
179
- ): Promise<string[]> {
180
- const result = await pool.query(
181
- `
182
- SELECT a.attname AS column_name
183
- FROM pg_constraint c
184
- JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
185
- WHERE c.contype = 'p'
186
- AND c.conrelid = $1::regclass
187
- ORDER BY a.attnum
188
- `,
189
- [pgTableRef(tableName, schema)]
190
- );
191
- return result.rows.map((r: { column_name: string }) => r.column_name);
192
- }
193
-
194
- async function queryForeignKeys(
195
- pool: Pool,
196
- tableName: string,
197
- schema: string = "public"
198
- ): Promise<ForeignKey[]> {
199
- const result = await pool.query(
200
- `
201
- SELECT
202
- a_from.attname AS from_column,
203
- cl_to.relname AS to_table,
204
- a_to.attname AS to_column,
205
- ns_to.nspname AS to_schema
206
- FROM pg_constraint c
207
- JOIN pg_attribute a_from
208
- ON a_from.attrelid = c.conrelid AND a_from.attnum = ANY(c.conkey)
209
- JOIN pg_class cl_to
210
- ON cl_to.oid = c.confrelid
211
- JOIN pg_namespace ns_to
212
- ON ns_to.oid = cl_to.relnamespace
213
- JOIN pg_attribute a_to
214
- ON a_to.attrelid = c.confrelid AND a_to.attnum = ANY(c.confkey)
215
- WHERE c.contype = 'f'
216
- AND c.conrelid = $1::regclass
217
- ORDER BY a_from.attname
218
- `,
219
- [pgTableRef(tableName, schema)]
220
- );
221
- return result.rows.map(
222
- (r: { from_column: string; to_table: string; to_column: string; to_schema: string }) => ({
223
- from_column: r.from_column,
224
- // Qualify FK target with schema when it differs from the profiled schema
225
- to_table: r.to_schema !== schema ? `${r.to_schema}.${r.to_table}` : r.to_table,
226
- to_column: r.to_column,
227
- source: "constraint" as const,
228
- })
229
- );
230
- }
231
-
232
- export async function listPostgresObjects(connectionString: string, schema: string = "public"): Promise<DatabaseObject[]> {
233
- const pool = new Pool({ connectionString, max: 1, connectionTimeoutMillis: 5000 });
234
- try {
235
- const result = await pool.query(
236
- `SELECT table_name, table_type FROM information_schema.tables
237
- WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
238
- ORDER BY table_name`,
239
- [schema]
240
- );
241
- const objects: DatabaseObject[] = result.rows.map((r: { table_name: string; table_type: string }) => ({
242
- name: r.table_name,
243
- type: r.table_type === "VIEW" ? "view" as const : "table" as const,
244
- }));
245
-
246
- // Materialized views are not in information_schema.tables — query pg_class directly
247
- try {
248
- const matviewResult = await pool.query(
249
- `SELECT c.relname AS table_name
250
- FROM pg_class c
251
- JOIN pg_namespace n ON n.oid = c.relnamespace
252
- WHERE n.nspname = $1 AND c.relkind = 'm'
253
- ORDER BY c.relname`,
254
- [schema]
255
- );
256
- for (const r of matviewResult.rows as { table_name: string }[]) {
257
- objects.push({ name: r.table_name, type: "materialized_view" });
258
- }
259
- } catch (mvErr) {
260
- console.warn(` Warning: Could not discover materialized views: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
261
- }
262
-
263
- return objects.sort((a, b) => a.name.localeCompare(b.name));
264
- } finally {
265
- await pool.end();
266
- }
267
- }
268
-
269
- export async function listMySQLObjects(connectionString: string): Promise<DatabaseObject[]> {
270
- // eslint-disable-next-line @typescript-eslint/no-require-imports
271
- const mysql = require("mysql2/promise");
272
- const pool = mysql.createPool({
273
- uri: connectionString,
274
- connectionLimit: 1,
275
- connectTimeout: 5000,
276
- });
277
- try {
278
- const [rows] = await pool.execute(
279
- `SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES
280
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
281
- ORDER BY TABLE_NAME`
282
- );
283
- return (rows as { TABLE_NAME: string; TABLE_TYPE: string }[]).map((r) => ({
284
- name: r.TABLE_NAME,
285
- type: r.TABLE_TYPE === "VIEW" ? "view" as const : "table" as const,
286
- }));
287
- } finally {
288
- await pool.end();
289
- }
290
- }
291
-
292
- export async function profilePostgres(
293
- connectionString: string,
294
- filterTables?: string[],
295
- prefetchedObjects?: DatabaseObject[],
296
- schema: string = "public"
297
- ): Promise<TableProfile[]> {
298
- const pool = new Pool({ connectionString, max: 3 });
299
- const profiles: TableProfile[] = [];
300
- const errors: { table: string; error: string }[] = [];
301
-
302
- let allObjects: DatabaseObject[];
303
- if (prefetchedObjects) {
304
- allObjects = prefetchedObjects;
305
- } else {
306
- const tablesResult = await pool.query(
307
- `SELECT table_name, table_type FROM information_schema.tables
308
- WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
309
- ORDER BY table_name`,
310
- [schema]
311
- );
312
- allObjects = tablesResult.rows.map((r: { table_name: string; table_type: string }) => ({
313
- name: r.table_name,
314
- type: r.table_type === "VIEW" ? "view" as const : "table" as const,
315
- }));
316
-
317
- // Materialized views are not in information_schema.tables — query pg_class directly
318
- try {
319
- const matviewResult = await pool.query(
320
- `SELECT c.relname AS table_name
321
- FROM pg_class c
322
- JOIN pg_namespace n ON n.oid = c.relnamespace
323
- WHERE n.nspname = $1 AND c.relkind = 'm'
324
- ORDER BY c.relname`,
325
- [schema]
326
- );
327
- for (const r of matviewResult.rows as { table_name: string }[]) {
328
- allObjects.push({ name: r.table_name, type: "materialized_view" });
329
- }
330
- } catch (mvErr) {
331
- console.warn(` Warning: Could not discover materialized views: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
332
- }
333
- allObjects.sort((a, b) => a.name.localeCompare(b.name));
334
- }
335
-
336
- const objectsToProfile = filterTables
337
- ? allObjects.filter((o) => filterTables.includes(o.name))
338
- : allObjects;
339
-
340
- for (const [i, obj] of objectsToProfile.entries()) {
341
- const table_name = obj.name;
342
- const objectType = obj.type;
343
- const objectLabel = objectType === "view" ? " [view]" : objectType === "materialized_view" ? " [matview]" : "";
344
- console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectLabel}...`);
345
-
346
- try {
347
- // Check matview populated status BEFORE COUNT(*) — unpopulated matviews throw on scan
348
- let matview_populated: boolean | undefined;
349
- if (objectType === "materialized_view") {
350
- try {
351
- const mvResult = await pool.query(
352
- `SELECT ispopulated FROM pg_matviews WHERE schemaname = $1 AND matviewname = $2`,
353
- [schema, table_name]
354
- );
355
- if (mvResult.rows.length > 0) {
356
- matview_populated = mvResult.rows[0].ispopulated;
357
- }
358
- } catch (mvErr) {
359
- console.warn(` Warning: Could not read matview status for ${table_name}: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
360
- }
361
- }
362
-
363
- // Skip COUNT(*) for unpopulated matviews — they error on table scan
364
- let rowCount: number;
365
- if (matview_populated === false) {
366
- rowCount = 0;
367
- console.log(` Materialized view ${table_name} is not populated — skipping data profiling`);
368
- } else {
369
- const countResult = await pool.query(
370
- `SELECT COUNT(*) as c FROM ${pgTableRef(table_name, schema)}`
371
- );
372
- rowCount = parseInt(countResult.rows[0].c, 10);
373
- }
374
-
375
- // Get primary keys and foreign keys from system catalogs (skip for views and matviews)
376
- let primaryKeyColumns: string[] = [];
377
- let foreignKeys: ForeignKey[] = [];
378
- if (objectType === "table") {
379
- try {
380
- primaryKeyColumns = await queryPrimaryKeys(pool, table_name, schema);
381
- } catch (pkErr) {
382
- console.warn(` Warning: Could not read PK constraints for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
383
- }
384
- try {
385
- foreignKeys = await queryForeignKeys(pool, table_name, schema);
386
- } catch (fkErr) {
387
- console.warn(` Warning: Could not read FK constraints for ${table_name}: ${fkErr instanceof Error ? fkErr.message : String(fkErr)}`);
388
- }
389
- }
390
-
391
- const fkLookup = new Map(
392
- foreignKeys.map((fk) => [fk.from_column, fk])
393
- );
394
-
395
- // information_schema.columns excludes materialized views in PostgreSQL,
396
- // so use pg_attribute + pg_type for matviews (#255)
397
- const colResult = objectType === "materialized_view"
398
- ? await pool.query(
399
- `
400
- SELECT a.attname AS column_name,
401
- pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
402
- CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable
403
- FROM pg_attribute a
404
- JOIN pg_class c ON c.oid = a.attrelid
405
- JOIN pg_namespace n ON n.oid = c.relnamespace
406
- WHERE n.nspname = $2
407
- AND c.relname = $1
408
- AND a.attnum > 0
409
- AND NOT a.attisdropped
410
- ORDER BY a.attnum
411
- `,
412
- [table_name, schema]
413
- )
414
- : await pool.query(
415
- `
416
- SELECT column_name, data_type, is_nullable
417
- FROM information_schema.columns
418
- WHERE table_name = $1 AND table_schema = $2
419
- ORDER BY ordinal_position
420
- `,
421
- [table_name, schema]
422
- );
423
-
424
- const columns: ColumnProfile[] = [];
425
-
426
- for (const col of colResult.rows) {
427
- let unique_count: number | null = null;
428
- let null_count: number | null = null;
429
- let sample_values: string[] = [];
430
- let isEnumLike = false;
431
-
432
- const isPK = primaryKeyColumns.includes(col.column_name);
433
- const fkInfo = fkLookup.get(col.column_name);
434
- const isFK = !!fkInfo;
435
-
436
- // Skip data profiling for unpopulated matviews — no data to scan
437
- if (matview_populated !== false) {
438
- try {
439
- const tableRef = pgTableRef(table_name, schema);
440
- const uq = await pool.query(
441
- `SELECT COUNT(DISTINCT "${col.column_name}") as c FROM ${tableRef}`
442
- );
443
- unique_count = parseInt(uq.rows[0].c, 10);
444
-
445
- const nc = await pool.query(
446
- `SELECT COUNT(*) as c FROM ${tableRef} WHERE "${col.column_name}" IS NULL`
447
- );
448
- null_count = parseInt(nc.rows[0].c, 10);
449
-
450
- // For enum-like columns, get ALL distinct values; otherwise sample 10
451
- const isTextType =
452
- col.data_type === "text" ||
453
- col.data_type === "character varying" ||
454
- col.data_type === "character";
455
- isEnumLike =
456
- isTextType &&
457
- unique_count !== null &&
458
- unique_count < 20 &&
459
- rowCount > 0 &&
460
- unique_count / rowCount <= 0.05;
461
-
462
- const sampleLimit = isEnumLike ? 100 : 10;
463
- const sv = await pool.query(
464
- `SELECT DISTINCT "${col.column_name}" as v FROM ${tableRef} WHERE "${col.column_name}" IS NOT NULL ORDER BY "${col.column_name}" LIMIT ${sampleLimit}`
465
- );
466
- sample_values = sv.rows.map((r: { v: unknown }) => String(r.v));
467
- } catch (colErr) {
468
- console.warn(` Warning: Could not profile column ${table_name}.${col.column_name}: ${colErr instanceof Error ? colErr.message : String(colErr)}`);
469
- }
470
- }
471
-
472
- columns.push({
473
- name: col.column_name,
474
- type: col.data_type,
475
- nullable: col.is_nullable === "YES",
476
- unique_count,
477
- null_count,
478
- sample_values,
479
- is_primary_key: isPK,
480
- is_foreign_key: isFK,
481
- fk_target_table: fkInfo?.to_table ?? null,
482
- fk_target_column: fkInfo?.to_column ?? null,
483
- is_enum_like: isEnumLike,
484
- profiler_notes: [],
485
- });
486
- }
487
-
488
- profiles.push({
489
- table_name,
490
- object_type: objectType,
491
- row_count: rowCount,
492
- columns,
493
- primary_key_columns: primaryKeyColumns,
494
- foreign_keys: foreignKeys,
495
- inferred_foreign_keys: [],
496
- profiler_notes: [],
497
- table_flags: { possibly_abandoned: false, possibly_denormalized: false },
498
- ...(matview_populated !== undefined ? { matview_populated } : {}),
499
- });
500
- } catch (err) {
501
- const msg = err instanceof Error ? err.message : String(err);
502
- console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
503
- errors.push({ table: table_name, error: msg });
504
- continue;
505
- }
506
- }
507
-
508
- // Batch-query partition metadata and attach to profiled tables
509
- const partitionMap = new Map<string, { strategy: "range" | "list" | "hash"; key: string }>();
510
- try {
511
- const partResult = await pool.query(
512
- `SELECT c.relname,
513
- CASE pt.partstrat WHEN 'r' THEN 'range' WHEN 'l' THEN 'list' WHEN 'h' THEN 'hash' ELSE pt.partstrat END as strategy,
514
- pg_get_partkeydef(c.oid) as partition_key
515
- FROM pg_partitioned_table pt
516
- JOIN pg_class c ON c.oid = pt.partrelid
517
- JOIN pg_namespace n ON n.oid = c.relnamespace
518
- WHERE n.nspname = $1`,
519
- [schema]
520
- );
521
-
522
- for (const r of partResult.rows as { relname: string; strategy: string; partition_key: string }[]) {
523
- if (r.strategy !== "range" && r.strategy !== "list" && r.strategy !== "hash") {
524
- console.warn(` Warning: Unrecognized partition strategy '${r.strategy}' for ${r.relname} — skipping`);
525
- continue;
526
- }
527
- partitionMap.set(r.relname, { strategy: r.strategy, key: r.partition_key });
528
- }
529
- } catch (partErr) {
530
- console.warn(` Warning: Could not read partition metadata: ${partErr instanceof Error ? partErr.message : String(partErr)}`);
531
- }
532
-
533
- const childrenMap = new Map<string, string[]>();
534
- try {
535
- const childResult = await pool.query(
536
- `SELECT p.relname as parent, c.relname as child
537
- FROM pg_inherits i
538
- JOIN pg_class c ON c.oid = i.inhrelid
539
- JOIN pg_class p ON p.oid = i.inhparent
540
- JOIN pg_namespace n ON n.oid = p.relnamespace
541
- WHERE n.nspname = $1
542
- ORDER BY p.relname, c.relname`,
543
- [schema]
544
- );
545
- for (const r of childResult.rows as { parent: string; child: string }[]) {
546
- const children = childrenMap.get(r.parent) ?? [];
547
- children.push(r.child);
548
- childrenMap.set(r.parent, children);
549
- }
550
- } catch (childErr) {
551
- console.warn(` Warning: Could not read partition children: ${childErr instanceof Error ? childErr.message : String(childErr)}`);
552
- }
553
-
554
- for (const profile of profiles) {
555
- const partInfo = partitionMap.get(profile.table_name);
556
- if (partInfo) {
557
- profile.partition_info = {
558
- strategy: partInfo.strategy,
559
- key: partInfo.key,
560
- children: childrenMap.get(profile.table_name) ?? [],
561
- };
562
- }
563
- }
564
-
565
- await pool.end();
566
-
567
- if (errors.length > 0) {
568
- console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
569
- for (const e of errors) {
570
- console.log(` - ${e.table}: ${e.error}`);
571
- }
572
- }
573
-
574
- return profiles;
575
- }
576
-
577
- // --- MySQL profiler ---
578
-
579
- async function queryMySQLPrimaryKeys(
580
- pool: import("mysql2/promise").Pool,
581
- tableName: string
582
- ): Promise<string[]> {
583
- const [rows] = await pool.execute(
584
- `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE
585
- WHERE TABLE_SCHEMA = DATABASE()
586
- AND TABLE_NAME = ?
587
- AND CONSTRAINT_NAME = 'PRIMARY'
588
- ORDER BY ORDINAL_POSITION`,
589
- [tableName]
590
- );
591
- return (rows as { COLUMN_NAME: string }[]).map((r) => r.COLUMN_NAME);
592
- }
593
-
594
- async function queryMySQLForeignKeys(
595
- pool: import("mysql2/promise").Pool,
596
- tableName: string
597
- ): Promise<ForeignKey[]> {
598
- const [rows] = await pool.execute(
599
- `SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
600
- FROM information_schema.KEY_COLUMN_USAGE
601
- WHERE TABLE_SCHEMA = DATABASE()
602
- AND TABLE_NAME = ?
603
- AND REFERENCED_TABLE_NAME IS NOT NULL
604
- ORDER BY COLUMN_NAME`,
605
- [tableName]
606
- );
607
- return (rows as { COLUMN_NAME: string; REFERENCED_TABLE_NAME: string; REFERENCED_COLUMN_NAME: string }[]).map((r) => ({
608
- from_column: r.COLUMN_NAME,
609
- to_table: r.REFERENCED_TABLE_NAME,
610
- to_column: r.REFERENCED_COLUMN_NAME,
611
- source: "constraint" as const,
612
- }));
613
- }
614
-
615
- export async function profileMySQL(
616
- connectionString: string,
617
- filterTables?: string[],
618
- prefetchedObjects?: DatabaseObject[]
619
- ): Promise<TableProfile[]> {
620
- // eslint-disable-next-line @typescript-eslint/no-require-imports
621
- const mysql = require("mysql2/promise");
622
- const pool = mysql.createPool({
623
- uri: connectionString,
624
- connectionLimit: 3,
625
- supportBigNumbers: true,
626
- bigNumberStrings: true,
627
- });
628
- const profiles: TableProfile[] = [];
629
- const errors: { table: string; error: string }[] = [];
630
-
631
- try {
632
- let allObjects: DatabaseObject[];
633
- if (prefetchedObjects) {
634
- allObjects = prefetchedObjects;
635
- } else {
636
- const [tablesRows] = await pool.execute(
637
- `SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES
638
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
639
- ORDER BY TABLE_NAME`
640
- );
641
- allObjects = (tablesRows as { TABLE_NAME: string; TABLE_TYPE: string }[]).map((r) => ({
642
- name: r.TABLE_NAME,
643
- type: r.TABLE_TYPE === "VIEW" ? "view" as const : "table" as const,
644
- }));
645
- }
646
-
647
- const objectsToProfile = filterTables
648
- ? allObjects.filter((o) => filterTables.includes(o.name))
649
- : allObjects;
650
-
651
- for (const [i, obj] of objectsToProfile.entries()) {
652
- const table_name = obj.name;
653
- const objectType = obj.type;
654
- console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectType === "view" ? " [view]" : ""}...`);
655
-
656
- try {
657
- const [countRows] = await pool.execute(
658
- `SELECT COUNT(*) as c FROM \`${table_name}\``
659
- );
660
- const rowCount = parseInt(String((countRows as { c: number }[])[0].c), 10);
661
-
662
- let primaryKeyColumns: string[] = [];
663
- let foreignKeys: ForeignKey[] = [];
664
- if (objectType === "table") {
665
- try {
666
- primaryKeyColumns = await queryMySQLPrimaryKeys(pool, table_name);
667
- } catch (pkErr) {
668
- console.warn(` Warning: Could not read PK constraints for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
669
- }
670
- try {
671
- foreignKeys = await queryMySQLForeignKeys(pool, table_name);
672
- } catch (fkErr) {
673
- console.warn(` Warning: Could not read FK constraints for ${table_name}: ${fkErr instanceof Error ? fkErr.message : String(fkErr)}`);
674
- }
675
- }
676
-
677
- const fkLookup = new Map(
678
- foreignKeys.map((fk) => [fk.from_column, fk])
679
- );
680
-
681
- const [colRows] = await pool.execute(
682
- `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_TYPE
683
- FROM information_schema.COLUMNS
684
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
685
- ORDER BY ORDINAL_POSITION`,
686
- [table_name]
687
- );
688
-
689
- const columns: ColumnProfile[] = [];
690
-
691
- for (const col of colRows as { COLUMN_NAME: string; DATA_TYPE: string; IS_NULLABLE: string; COLUMN_TYPE: string }[]) {
692
- let unique_count: number | null = null;
693
- let null_count: number | null = null;
694
- let sample_values: string[] = [];
695
- let isEnumLike = false;
696
-
697
- const isPK = primaryKeyColumns.includes(col.COLUMN_NAME);
698
- const fkInfo = fkLookup.get(col.COLUMN_NAME);
699
- const isFK = !!fkInfo;
700
-
701
- try {
702
- const [uqRows] = await pool.execute(
703
- `SELECT COUNT(DISTINCT \`${col.COLUMN_NAME}\`) as c FROM \`${table_name}\``
704
- );
705
- unique_count = parseInt(String((uqRows as { c: number }[])[0].c), 10);
706
-
707
- const [ncRows] = await pool.execute(
708
- `SELECT COUNT(*) as c FROM \`${table_name}\` WHERE \`${col.COLUMN_NAME}\` IS NULL`
709
- );
710
- null_count = parseInt(String((ncRows as { c: number }[])[0].c), 10);
711
-
712
- // Enum-like detection: text/enum/set columns with <20 distinct values and <=5% cardinality
713
- const dataType = col.DATA_TYPE.toLowerCase();
714
- const isTextType =
715
- dataType === "varchar" ||
716
- dataType === "char" ||
717
- dataType === "text" ||
718
- dataType === "tinytext" ||
719
- dataType === "mediumtext" ||
720
- dataType === "longtext" ||
721
- dataType === "enum" ||
722
- dataType === "set";
723
- isEnumLike =
724
- isTextType &&
725
- unique_count !== null &&
726
- unique_count < 20 &&
727
- rowCount > 0 &&
728
- unique_count / rowCount <= 0.05;
729
-
730
- const sampleLimit = isEnumLike ? 100 : 10;
731
- const [svRows] = await pool.execute(
732
- `SELECT DISTINCT \`${col.COLUMN_NAME}\` as v FROM \`${table_name}\` WHERE \`${col.COLUMN_NAME}\` IS NOT NULL ORDER BY \`${col.COLUMN_NAME}\` LIMIT ${sampleLimit}`
733
- );
734
- sample_values = (svRows as { v: unknown }[]).map((r) => String(r.v));
735
- } catch (colErr) {
736
- console.warn(` Warning: Could not profile column ${table_name}.${col.COLUMN_NAME}: ${colErr instanceof Error ? colErr.message : String(colErr)}`);
737
- }
738
-
739
- columns.push({
740
- name: col.COLUMN_NAME,
741
- type: col.DATA_TYPE,
742
- nullable: col.IS_NULLABLE === "YES",
743
- unique_count,
744
- null_count,
745
- sample_values,
746
- is_primary_key: isPK,
747
- is_foreign_key: isFK,
748
- fk_target_table: fkInfo?.to_table ?? null,
749
- fk_target_column: fkInfo?.to_column ?? null,
750
- is_enum_like: isEnumLike,
751
- profiler_notes: [],
752
- });
753
- }
754
-
755
- profiles.push({
756
- table_name,
757
- object_type: objectType,
758
- row_count: rowCount,
759
- columns,
760
- primary_key_columns: primaryKeyColumns,
761
- foreign_keys: foreignKeys,
762
- inferred_foreign_keys: [],
763
- profiler_notes: [],
764
- table_flags: { possibly_abandoned: false, possibly_denormalized: false },
765
- });
766
- } catch (err) {
767
- const msg = err instanceof Error ? err.message : String(err);
768
- // Fail fast on connection-level errors that will affect all remaining tables
769
- if (/PROTOCOL_CONNECTION_LOST|ER_SERVER_SHUTDOWN|ER_NET_READ_ERROR|ER_NET_WRITE_ERROR|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT/i.test(msg)) {
770
- throw new Error(`Fatal database error while profiling ${table_name}: ${msg}`, { cause: err });
771
- }
772
- console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
773
- errors.push({ table: table_name, error: msg });
774
- continue;
775
- }
776
- }
777
-
778
- } finally {
779
- await pool.end().catch((err: unknown) => {
780
- console.warn(`[atlas] MySQL pool cleanup warning: ${err instanceof Error ? err.message : String(err)}`);
781
- });
782
- }
783
-
784
- if (errors.length > 0) {
785
- console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
786
- for (const e of errors) {
787
- console.log(` - ${e.table}: ${e.error}`);
788
- }
789
- }
790
188
 
791
- return profiles;
792
- }
793
189
 
794
190
  // --- ClickHouse profiler ---
795
191
 
796
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
797
- type ClickHouseClient = { query: (opts: { query: string; format: string }) => Promise<{ json: () => Promise<any> }>; close: () => Promise<void> };
192
+ type ClickHouseClient = { query: (opts: { query: string; format: string }) => Promise<{ json: () => Promise<{ data: Record<string, unknown>[] }> }>; close: () => Promise<void> };
798
193
 
799
194
  /** Run a single query against ClickHouse and return rows. */
800
195
  async function clickhouseQuery<T = Record<string, unknown>>(
@@ -816,7 +211,7 @@ function rewriteClickHouseUrl(url: string): string {
816
211
  return url.replace(/^clickhouses:\/\//, "https://").replace(/^clickhouse:\/\//, "http://");
817
212
  }
818
213
 
819
- export async function listClickHouseObjects(connectionString: string): Promise<DatabaseObject[]> {
214
+ async function listClickHouseObjects(connectionString: string): Promise<DatabaseObject[]> {
820
215
  // eslint-disable-next-line @typescript-eslint/no-require-imports
821
216
  const { createClient } = require("@clickhouse/client");
822
217
  const client = createClient({ url: rewriteClickHouseUrl(connectionString) });
@@ -862,17 +257,18 @@ function mapClickHouseType(chType: string): string {
862
257
  return "string";
863
258
  }
864
259
 
865
- export async function profileClickHouse(
260
+ async function profileClickHouse(
866
261
  connectionString: string,
867
262
  filterTables?: string[],
868
- prefetchedObjects?: DatabaseObject[]
869
- ): Promise<TableProfile[]> {
263
+ prefetchedObjects?: DatabaseObject[],
264
+ progress?: ProfileProgressCallbacks
265
+ ): Promise<ProfilingResult> {
870
266
  // eslint-disable-next-line @typescript-eslint/no-require-imports
871
267
  const { createClient } = require("@clickhouse/client");
872
268
  const client = createClient({ url: rewriteClickHouseUrl(connectionString) });
873
269
 
874
270
  const profiles: TableProfile[] = [];
875
- const errors: { table: string; error: string }[] = [];
271
+ const errors: ProfileError[] = [];
876
272
 
877
273
  try {
878
274
  let allObjects: DatabaseObject[];
@@ -896,11 +292,18 @@ export async function profileClickHouse(
896
292
  ? allObjects.filter((o) => filterTables.includes(o.name))
897
293
  : allObjects;
898
294
 
295
+ progress?.onStart(objectsToProfile.length);
296
+
899
297
  for (const [i, obj] of objectsToProfile.entries()) {
900
298
  const table_name = obj.name;
901
299
  const objectType = obj.type;
902
300
  const safeTable = table_name.replace(/'/g, "''");
903
- console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectType === "view" ? " [view]" : ""}...`);
301
+ const objectLabel = objectType === "view" ? " [view]" : "";
302
+ if (progress) {
303
+ progress.onTableStart(table_name + objectLabel, i, objectsToProfile.length);
304
+ } else {
305
+ console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectLabel}...`);
306
+ }
904
307
 
905
308
  try {
906
309
  const countRows = await clickhouseQuery<{ c: string }>(
@@ -916,6 +319,7 @@ export async function profileClickHouse(
916
319
  try {
917
320
  primaryKeyColumns = await queryClickHousePrimaryKeys(client, table_name);
918
321
  } catch (pkErr) {
322
+ if (isFatalConnectionError(pkErr)) throw pkErr;
919
323
  console.warn(` Warning: Could not read PK columns for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
920
324
  }
921
325
  }
@@ -974,6 +378,7 @@ export async function profileClickHouse(
974
378
  );
975
379
  sample_values = svRows.map((r) => String(r.v));
976
380
  } catch (colErr) {
381
+ if (isFatalConnectionError(colErr)) throw colErr;
977
382
  console.warn(` Warning: Could not profile column ${table_name}.${col.name}: ${colErr instanceof Error ? colErr.message : String(colErr)}`);
978
383
  }
979
384
 
@@ -1004,12 +409,18 @@ export async function profileClickHouse(
1004
409
  profiler_notes: [],
1005
410
  table_flags: { possibly_abandoned: false, possibly_denormalized: false },
1006
411
  });
412
+ progress?.onTableDone(table_name, i, objectsToProfile.length);
1007
413
  } catch (err) {
1008
414
  const msg = err instanceof Error ? err.message : String(err);
1009
- if (/ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT/i.test(msg)) {
415
+ // Fail fast on connection-level errors that will affect all remaining tables
416
+ if (isFatalConnectionError(err)) {
1010
417
  throw new Error(`Fatal database error while profiling ${table_name}: ${msg}`, { cause: err });
1011
418
  }
1012
- console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
419
+ if (progress) {
420
+ progress.onTableError(table_name, msg, i, objectsToProfile.length);
421
+ } else {
422
+ console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
423
+ }
1013
424
  errors.push({ table: table_name, error: msg });
1014
425
  continue;
1015
426
  }
@@ -1020,14 +431,7 @@ export async function profileClickHouse(
1020
431
  });
1021
432
  }
1022
433
 
1023
- if (errors.length > 0) {
1024
- console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
1025
- for (const e of errors) {
1026
- console.log(` - ${e.table}: ${e.error}`);
1027
- }
1028
- }
1029
-
1030
- return profiles;
434
+ return { profiles, errors };
1031
435
  }
1032
436
 
1033
437
  // --- Snowflake profiler ---
@@ -1087,7 +491,7 @@ async function createSnowflakePool(connectionString: string, max = 1) {
1087
491
  return { pool, opts };
1088
492
  }
1089
493
 
1090
- export async function listSnowflakeObjects(connectionString: string): Promise<DatabaseObject[]> {
494
+ async function listSnowflakeObjects(connectionString: string): Promise<DatabaseObject[]> {
1091
495
  const { pool } = await createSnowflakePool(connectionString, 1);
1092
496
 
1093
497
  try {
@@ -1167,16 +571,17 @@ function mapSnowflakeType(sfType: string): string {
1167
571
  return "text";
1168
572
  }
1169
573
 
1170
- export async function profileSnowflake(
574
+ async function profileSnowflake(
1171
575
  connectionString: string,
1172
576
  filterTables?: string[],
1173
577
  prefetchedObjects?: DatabaseObject[],
1174
- ): Promise<TableProfile[]> {
578
+ progress?: ProfileProgressCallbacks
579
+ ): Promise<ProfilingResult> {
1175
580
  const { pool, opts } = await createSnowflakePool(connectionString, 3);
1176
581
 
1177
582
 
1178
583
  const profiles: TableProfile[] = [];
1179
- const errors: { table: string; error: string }[] = [];
584
+ const errors: ProfileError[] = [];
1180
585
  const escId = (name: string) => name.replace(/"/g, '""');
1181
586
 
1182
587
  try {
@@ -1200,10 +605,17 @@ export async function profileSnowflake(
1200
605
  ? allObjects.filter((o) => filterTables.includes(o.name))
1201
606
  : allObjects;
1202
607
 
608
+ progress?.onStart(objectsToProfile.length);
609
+
1203
610
  for (const [i, obj] of objectsToProfile.entries()) {
1204
611
  const table_name = obj.name;
1205
612
  const objectType = obj.type;
1206
- console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectType === "view" ? " [view]" : ""}...`);
613
+ const objectLabel = objectType === "view" ? " [view]" : "";
614
+ if (progress) {
615
+ progress.onTableStart(table_name + objectLabel, i, objectsToProfile.length);
616
+ } else {
617
+ console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectLabel}...`);
618
+ }
1207
619
 
1208
620
  try {
1209
621
  let primaryKeyColumns: string[] = [];
@@ -1212,11 +624,13 @@ export async function profileSnowflake(
1212
624
  try {
1213
625
  primaryKeyColumns = await querySnowflakePrimaryKeys(pool, table_name, opts.database, opts.schema);
1214
626
  } catch (pkErr) {
627
+ if (isFatalConnectionError(pkErr)) throw pkErr;
1215
628
  console.warn(` Warning: Could not read PK constraints for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
1216
629
  }
1217
630
  try {
1218
631
  foreignKeys = await querySnowflakeForeignKeys(pool, table_name, opts.database, opts.schema);
1219
632
  } catch (fkErr) {
633
+ if (isFatalConnectionError(fkErr)) throw fkErr;
1220
634
  console.warn(` Warning: Could not read FK constraints for ${table_name}: ${fkErr instanceof Error ? fkErr.message : String(fkErr)}`);
1221
635
  }
1222
636
  }
@@ -1253,11 +667,13 @@ export async function profileSnowflake(
1253
667
  });
1254
668
  }
1255
669
  } catch (bulkErr) {
670
+ if (isFatalConnectionError(bulkErr)) throw bulkErr;
1256
671
  console.warn(` Warning: Bulk stats query failed for ${table_name}, falling back to row count only: ${bulkErr instanceof Error ? bulkErr.message : String(bulkErr)}`);
1257
672
  try {
1258
673
  const countResult = await snowflakeQuery(pool, `SELECT COUNT(*) as "RC" FROM "${escId(table_name)}"`);
1259
674
  rowCount = parseInt(String(countResult.rows[0]?.RC ?? "0"), 10);
1260
675
  } catch (countErr) {
676
+ if (isFatalConnectionError(countErr)) throw countErr;
1261
677
  console.warn(` Warning: Row count query also failed for ${table_name}: ${countErr instanceof Error ? countErr.message : String(countErr)}`);
1262
678
  }
1263
679
  }
@@ -1266,6 +682,7 @@ export async function profileSnowflake(
1266
682
  const countResult = await snowflakeQuery(pool, `SELECT COUNT(*) as "RC" FROM "${escId(table_name)}"`);
1267
683
  rowCount = parseInt(String(countResult.rows[0]?.RC ?? "0"), 10);
1268
684
  } catch (countErr) {
685
+ if (isFatalConnectionError(countErr)) throw countErr;
1269
686
  console.warn(` Warning: Row count query failed for ${table_name}: ${countErr instanceof Error ? countErr.message : String(countErr)}`);
1270
687
  }
1271
688
  }
@@ -1298,6 +715,7 @@ export async function profileSnowflake(
1298
715
  samplesMap.get(cn)!.push(String(row.V));
1299
716
  }
1300
717
  } catch (sampleErr) {
718
+ if (isFatalConnectionError(sampleErr)) throw sampleErr;
1301
719
  console.warn(` Warning: Batched sample values query failed for ${table_name} (${colMeta.length} columns affected): ${sampleErr instanceof Error ? sampleErr.message : String(sampleErr)}`);
1302
720
  }
1303
721
  }
@@ -1335,12 +753,19 @@ export async function profileSnowflake(
1335
753
  profiler_notes: [],
1336
754
  table_flags: { possibly_abandoned: false, possibly_denormalized: false },
1337
755
  });
756
+ progress?.onTableDone(table_name, i, objectsToProfile.length);
1338
757
  } catch (err) {
1339
758
  const msg = err instanceof Error ? err.message : String(err);
1340
- if (/ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT|390100|390114|250001/i.test(msg)) {
759
+ // Fail fast on connection-level errors that will affect all remaining tables
760
+ // Snowflake-specific: 390100 = auth token expired, 390114 = auth token invalid, 250001 = connection failure
761
+ if (isFatalConnectionError(err) || /390100|390114|250001/.test(msg)) {
1341
762
  throw new Error(`Fatal database error while profiling ${table_name}: ${msg}`, { cause: err });
1342
763
  }
1343
- console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
764
+ if (progress) {
765
+ progress.onTableError(table_name, msg, i, objectsToProfile.length);
766
+ } else {
767
+ console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
768
+ }
1344
769
  errors.push({ table: table_name, error: msg });
1345
770
  continue;
1346
771
  }
@@ -1354,58 +779,15 @@ export async function profileSnowflake(
1354
779
  }
1355
780
  }
1356
781
 
1357
- if (errors.length > 0) {
1358
- console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
1359
- for (const e of errors) {
1360
- console.log(` - ${e.table}: ${e.error}`);
1361
- }
1362
- }
1363
-
1364
- return profiles;
782
+ return { profiles, errors };
1365
783
  }
1366
784
 
1367
785
  // --- Salesforce profiler ---
1368
786
 
1369
- /** Map Salesforce field types to semantic layer types. */
1370
- function mapSalesforceFieldType(sfType: string): string {
1371
- const lower = sfType.toLowerCase();
1372
- switch (lower) {
1373
- case "int":
1374
- case "long":
1375
- return "integer";
1376
- case "double":
1377
- case "currency":
1378
- case "percent":
1379
- return "real";
1380
- case "boolean":
1381
- return "boolean";
1382
- case "date":
1383
- case "datetime":
1384
- case "time":
1385
- return "date";
1386
- case "string":
1387
- case "id":
1388
- case "reference":
1389
- case "textarea":
1390
- case "url":
1391
- case "email":
1392
- case "phone":
1393
- case "picklist":
1394
- case "multipicklist":
1395
- case "combobox":
1396
- case "encryptedstring":
1397
- case "base64":
1398
- return "text";
1399
- default:
1400
- return "text";
1401
- }
1402
- }
1403
-
1404
- export async function listSalesforceObjects(connectionString: string): Promise<DatabaseObject[]> {
787
+ async function listSalesforceObjects(connectionString: string): Promise<DatabaseObject[]> {
1405
788
  const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
1406
789
  const config = parseSalesforceURL(connectionString);
1407
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1408
- const source: any = createSalesforceConnection(config);
790
+ const source = createSalesforceConnection(config);
1409
791
  try {
1410
792
  const objects = await source.listObjects();
1411
793
  return objects.map((obj: { name: string }) => ({
@@ -1417,18 +799,18 @@ export async function listSalesforceObjects(connectionString: string): Promise<D
1417
799
  }
1418
800
  }
1419
801
 
1420
- export async function profileSalesforce(
802
+ async function profileSalesforce(
1421
803
  connectionString: string,
1422
804
  filterTables?: string[],
1423
805
  prefetchedObjects?: DatabaseObject[],
1424
- ): Promise<TableProfile[]> {
806
+ progress?: ProfileProgressCallbacks
807
+ ): Promise<ProfilingResult> {
1425
808
  const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
1426
809
  const config = parseSalesforceURL(connectionString);
1427
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1428
- const source: any = createSalesforceConnection(config);
810
+ const source = createSalesforceConnection(config);
1429
811
 
1430
812
  const profiles: TableProfile[] = [];
1431
- const errors: { table: string; error: string }[] = [];
813
+ const errors: ProfileError[] = [];
1432
814
 
1433
815
  try {
1434
816
  let allObjects: DatabaseObject[];
@@ -1446,9 +828,15 @@ export async function profileSalesforce(
1446
828
  ? allObjects.filter((o) => filterTables.includes(o.name))
1447
829
  : allObjects;
1448
830
 
831
+ progress?.onStart(objectsToProfile.length);
832
+
1449
833
  for (const [i, obj] of objectsToProfile.entries()) {
1450
834
  const objectName = obj.name;
1451
- console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${objectName}...`);
835
+ if (progress) {
836
+ progress.onTableStart(objectName, i, objectsToProfile.length);
837
+ } else {
838
+ console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${objectName}...`);
839
+ }
1452
840
 
1453
841
  try {
1454
842
  const desc = await source.describe(objectName);
@@ -1464,14 +852,14 @@ export async function profileSalesforce(
1464
852
  rowCount = parseInt(String(countVal ?? "0"), 10);
1465
853
  }
1466
854
  } catch (countErr) {
855
+ if (isFatalConnectionError(countErr)) throw countErr;
1467
856
  console.warn(` Warning: Could not get row count for ${objectName}: ${countErr instanceof Error ? countErr.message : String(countErr)}`);
1468
857
  }
1469
858
 
1470
859
  const foreignKeys: ForeignKey[] = [];
1471
860
  const primaryKeyColumns: string[] = [];
1472
861
 
1473
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1474
- const columns: ColumnProfile[] = desc.fields.map((field: any) => {
862
+ const columns: ColumnProfile[] = desc.fields.map((field: SObjectField) => {
1475
863
  const isPK = field.name === "Id";
1476
864
  if (isPK) primaryKeyColumns.push(field.name);
1477
865
 
@@ -1489,8 +877,7 @@ export async function profileSalesforce(
1489
877
 
1490
878
  // For picklist fields, extract active values as sample_values
1491
879
  const sampleValues = isEnumLike
1492
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1493
- ? field.picklistValues.filter((pv: any) => pv.active).map((pv: any) => pv.value)
880
+ ? field.picklistValues.filter((pv) => pv.active).map((pv) => pv.value)
1494
881
  : [];
1495
882
 
1496
883
  return {
@@ -1501,953 +888,57 @@ export async function profileSalesforce(
1501
888
  null_count: null,
1502
889
  sample_values: sampleValues,
1503
890
  is_primary_key: isPK,
1504
- is_foreign_key: isFK,
1505
- fk_target_table: isFK ? field.referenceTo[0] : null,
1506
- fk_target_column: isFK ? "Id" : null,
1507
- is_enum_like: isEnumLike,
1508
- profiler_notes: [],
1509
- };
1510
- });
1511
-
1512
- profiles.push({
1513
- table_name: objectName,
1514
- object_type: "table",
1515
- row_count: rowCount,
1516
- columns,
1517
- primary_key_columns: primaryKeyColumns,
1518
- foreign_keys: foreignKeys,
1519
- inferred_foreign_keys: [],
1520
- profiler_notes: [],
1521
- table_flags: { possibly_abandoned: false, possibly_denormalized: false },
1522
- });
1523
- } catch (err) {
1524
- const msg = err instanceof Error ? err.message : String(err);
1525
- if (/ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT/i.test(msg)) {
1526
- throw new Error(`Fatal Salesforce error while profiling ${objectName}: ${msg}`, { cause: err });
1527
- }
1528
- console.error(` Warning: Failed to profile ${objectName}: ${msg}`);
1529
- errors.push({ table: objectName, error: msg });
1530
- continue;
1531
- }
1532
- }
1533
- } finally {
1534
- await source.close();
1535
- }
1536
-
1537
- if (errors.length > 0) {
1538
- console.log(`\nWarning: ${errors.length} object(s) failed to profile:`);
1539
- for (const e of errors) {
1540
- console.log(` - ${e.table}: ${e.error}`);
1541
- }
1542
- }
1543
-
1544
- return profiles;
1545
- }
1546
-
1547
- // --- Pluralization / singularization (shared by heuristics + YAML generation) ---
1548
-
1549
- /**
1550
- * Plural → singular lookup for irregular English words.
1551
- * Also used by YAML generation (singularize) and heuristics (inferForeignKeys).
1552
- */
1553
- const IRREGULAR_PLURALS: Record<string, string> = {
1554
- people: "person",
1555
- children: "child",
1556
- men: "man",
1557
- women: "woman",
1558
- mice: "mouse",
1559
- data: "datum",
1560
- criteria: "criterion",
1561
- analyses: "analysis",
1562
- };
1563
-
1564
- /** Derived inverse: singular → plural, built once from IRREGULAR_PLURALS. */
1565
- const IRREGULAR_SINGULARS_TO_PLURALS: Record<string, string> = Object.fromEntries(
1566
- Object.entries(IRREGULAR_PLURALS).map(([plural, singular]) => [singular, plural])
1567
- );
1568
-
1569
- export function pluralize(word: string): string {
1570
- const lower = word.toLowerCase();
1571
- if (IRREGULAR_SINGULARS_TO_PLURALS[lower]) return IRREGULAR_SINGULARS_TO_PLURALS[lower];
1572
- if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower))
1573
- return word.slice(0, -1) + "ies";
1574
- if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("sh") || lower.endsWith("ch"))
1575
- return word + "es";
1576
- return word + "s";
1577
- }
1578
-
1579
- export function singularize(word: string): string {
1580
- const lower = word.toLowerCase();
1581
- if (IRREGULAR_PLURALS[lower]) return IRREGULAR_PLURALS[lower];
1582
- if (lower.endsWith("ies")) return word.slice(0, -3) + "y";
1583
- if (lower.endsWith("ses") || lower.endsWith("xes") || lower.endsWith("zes"))
1584
- return word.slice(0, -2);
1585
- if (lower.endsWith("s") && !lower.endsWith("ss") && !lower.endsWith("us") && !lower.endsWith("is")) return word.slice(0, -1);
1586
- return word;
1587
- }
1588
-
1589
- // --- Profiler heuristics (pure functions on TableProfile[]) ---
1590
-
1591
- /**
1592
- * For each `*_id` column without an existing FK constraint, try to match
1593
- * the prefix to a table name (singular or plural). Only infer when the
1594
- * target table has a PK column named `id`.
1595
- */
1596
- export function inferForeignKeys(profiles: TableProfile[]): void {
1597
- // Only tables (not views/matviews) can be FK targets — views have no PKs
1598
- const tableMap = new Map(
1599
- profiles.filter((p) => !isViewLike(p)).map((p) => [p.table_name, p])
1600
- );
1601
-
1602
- for (const profile of profiles) {
1603
- if (isViewLike(profile)) continue;
1604
-
1605
- const constrainedCols = new Set(profile.foreign_keys.map((fk) => fk.from_column));
1606
-
1607
- for (const col of profile.columns) {
1608
- if (!col.name.endsWith("_id")) continue;
1609
- if (constrainedCols.has(col.name)) continue;
1610
- if (col.is_primary_key) continue;
1611
-
1612
- const prefix = col.name.slice(0, -3); // strip "_id"
1613
- if (!prefix) continue;
1614
-
1615
- // Try direct match, plural, singular
1616
- const candidates = [prefix, pluralize(prefix), singularize(prefix)];
1617
- let targetTable: TableProfile | undefined;
1618
- for (const candidate of candidates) {
1619
- targetTable = tableMap.get(candidate);
1620
- if (targetTable) break;
1621
- }
1622
-
1623
- if (!targetTable) continue;
1624
-
1625
- // Only infer when target has PK column named "id"
1626
- const hasPkId = targetTable.primary_key_columns.includes("id");
1627
- if (!hasPkId) continue;
1628
-
1629
- const inferredFK: ForeignKey = {
1630
- from_column: col.name,
1631
- to_table: targetTable.table_name,
1632
- to_column: "id",
1633
- source: "inferred",
1634
- };
1635
-
1636
- profile.inferred_foreign_keys.push(inferredFK);
1637
-
1638
- col.profiler_notes.push(
1639
- `Likely FK to ${targetTable.table_name}.id (inferred from column name, no constraint exists)`
1640
- );
1641
- }
1642
- }
1643
- }
1644
-
1645
- const ABANDONED_NAME_PATTERNS = [
1646
- /^old_/,
1647
- /^temp_/,
1648
- /^legacy_/,
1649
- /_legacy$/,
1650
- /_backup$/,
1651
- /_archive$/,
1652
- /_v\d+$/,
1653
- ];
1654
-
1655
- /**
1656
- * Flag tables whose names match legacy/temp patterns AND have zero inbound FKs
1657
- * (both constraint and inferred). Views and matviews are excluded — they cannot be abandoned.
1658
- */
1659
- export function detectAbandonedTables(profiles: TableProfile[]): void {
1660
- // Build set of tables referenced by any FK (constraint or inferred)
1661
- const referencedTables = new Set<string>();
1662
- for (const p of profiles) {
1663
- for (const fk of p.foreign_keys) referencedTables.add(fk.to_table);
1664
- for (const fk of p.inferred_foreign_keys) referencedTables.add(fk.to_table);
1665
- }
1666
-
1667
- for (const profile of profiles) {
1668
- if (isViewLike(profile)) continue;
1669
-
1670
- const nameMatches = ABANDONED_NAME_PATTERNS.some((pat) =>
1671
- pat.test(profile.table_name)
1672
- );
1673
- if (!nameMatches) continue;
1674
-
1675
- const hasInboundFKs = referencedTables.has(profile.table_name);
1676
- if (hasInboundFKs) continue;
1677
-
1678
- profile.table_flags.possibly_abandoned = true;
1679
- profile.profiler_notes.push(
1680
- `Possibly abandoned: name matches legacy/temp pattern and no other tables reference it`
1681
- );
1682
- }
1683
- }
1684
-
1685
- /**
1686
- * For enum-like columns, detect case-inconsistent values
1687
- * (e.g., 'Technology', 'tech', 'TECHNOLOGY' all map to the same lowercase form).
1688
- */
1689
- export function detectEnumInconsistency(profiles: TableProfile[]): void {
1690
- for (const profile of profiles) {
1691
- for (const col of profile.columns) {
1692
- if (!col.is_enum_like) continue;
1693
- if (col.sample_values.length === 0) continue;
1694
-
1695
- // Group by lowercase form
1696
- const groups = new Map<string, string[]>();
1697
- for (const val of col.sample_values) {
1698
- const lower = val.toLowerCase();
1699
- const existing = groups.get(lower) ?? [];
1700
- existing.push(val);
1701
- groups.set(lower, existing);
1702
- }
1703
-
1704
- // Find groups with multiple original forms
1705
- const inconsistencies: string[] = [];
1706
- for (const [, originals] of groups) {
1707
- if (originals.length > 1) {
1708
- inconsistencies.push(originals.join(", "));
1709
- }
1710
- }
1711
-
1712
- if (inconsistencies.length > 0) {
1713
- col.profiler_notes.push(
1714
- `Case-inconsistent enum values: [${inconsistencies.join("; ")}]. Consider using LOWER() for grouping`
1715
- );
1716
- }
1717
- }
1718
- }
1719
- }
1720
-
1721
- const DENORMALIZED_NAME_PATTERNS = [
1722
- /_denormalized$/,
1723
- /_cache$/,
1724
- /_summary$/,
1725
- /_stats$/,
1726
- /_rollup$/,
1727
- ];
1728
-
1729
- /**
1730
- * Flag tables whose names match denormalization patterns. Views and matviews are excluded.
1731
- */
1732
- export function detectDenormalizedTables(profiles: TableProfile[]): void {
1733
- for (const profile of profiles) {
1734
- if (isViewLike(profile)) continue;
1735
-
1736
- const nameMatches = DENORMALIZED_NAME_PATTERNS.some((pat) =>
1737
- pat.test(profile.table_name)
1738
- );
1739
- if (!nameMatches) continue;
1740
-
1741
- profile.table_flags.possibly_denormalized = true;
1742
- profile.profiler_notes.push(
1743
- `Possibly denormalized/materialized table: name matches reporting pattern. Data may duplicate other tables`
1744
- );
1745
- }
1746
- }
1747
-
1748
- /**
1749
- * Orchestrate all profiler heuristics. Initializes empty arrays/flags,
1750
- * then runs all detectors in sequence.
1751
- */
1752
- export function analyzeTableProfiles(profiles: TableProfile[]): void {
1753
- // Reset containers on all profiles (clear any prior run)
1754
- for (const p of profiles) {
1755
- p.inferred_foreign_keys = [];
1756
- p.profiler_notes = [];
1757
- p.table_flags = { possibly_abandoned: false, possibly_denormalized: false };
1758
- for (const col of p.columns) {
1759
- col.profiler_notes = [];
1760
- }
1761
- }
1762
-
1763
- inferForeignKeys(profiles);
1764
- detectAbandonedTables(profiles);
1765
- detectEnumInconsistency(profiles);
1766
- detectDenormalizedTables(profiles);
1767
- }
1768
-
1769
- // --- Generate YAML from profile ---
1770
-
1771
- function entityName(tableName: string): string {
1772
- return tableName
1773
- .split("_")
1774
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1775
- .join("");
1776
- }
1777
-
1778
- export function generateEntityYAML(
1779
- profile: TableProfile,
1780
- allProfiles: TableProfile[],
1781
- dbType: DBType,
1782
- schema: string = "public",
1783
- source?: string,
1784
- ): string {
1785
- const name = entityName(profile.table_name);
1786
- // DuckDB's default schema is "main" — don't qualify with it (same as Postgres "public")
1787
- const qualifiedTable = schema !== "public" && schema !== "main" ? `${schema}.${profile.table_name}` : profile.table_name;
1788
-
1789
- // Build dimensions
1790
- const dimensions: Record<string, unknown>[] = profile.columns.map((col) => {
1791
- const dim: Record<string, unknown> = {
1792
- name: col.name,
1793
- sql: col.name,
1794
- type: dbType === "salesforce" ? mapSalesforceFieldType(col.type) : mapSQLType(col.type),
1795
- };
1796
-
1797
- // Description
1798
- if (col.is_primary_key) {
1799
- dim.description = `Primary key`;
1800
- dim.primary_key = true;
1801
- } else if (col.is_foreign_key) {
1802
- dim.description = `Foreign key to ${col.fk_target_table}`;
1803
- }
1804
-
1805
- if (col.unique_count !== null) dim.unique_count = col.unique_count;
1806
- if (col.null_count !== null && col.null_count > 0)
1807
- dim.null_count = col.null_count;
1808
- if (col.sample_values.length > 0) {
1809
- dim.sample_values = col.is_enum_like
1810
- ? col.sample_values
1811
- : col.sample_values.slice(0, 8);
1812
- }
1813
-
1814
- return dim;
1815
- });
1816
-
1817
- // Build virtual dimensions — dialect-aware CASE bucketing and date extractions
1818
- const virtualDims: Record<string, unknown>[] = [];
1819
- for (const col of profile.columns) {
1820
- if (col.is_primary_key || col.is_foreign_key) continue;
1821
- const mappedType = dbType === "salesforce" ? mapSalesforceFieldType(col.type) : mapSQLType(col.type);
1822
-
1823
- if (mappedType === "number" && !col.name.endsWith("_id") && dbType !== "salesforce") {
1824
- const label = col.name.replace(/_/g, " ");
1825
- if (dbType === "mysql") {
1826
- // MySQL: simple fixed-boundary bucketing (no PERCENTILE_CONT)
1827
- virtualDims.push({
1828
- name: `${col.name}_bucket`,
1829
- sql: `CASE\n WHEN ${col.name} IS NULL THEN 'Unknown'\n WHEN ${col.name} < (SELECT AVG(${col.name}) * 0.5 FROM ${qualifiedTable}) THEN 'Low'\n WHEN ${col.name} < (SELECT AVG(${col.name}) * 1.5 FROM ${qualifiedTable}) THEN 'Medium'\n ELSE 'High'\nEND`,
1830
- type: "string",
1831
- description: `${label} bucketed into Low/Medium/High`,
1832
- virtual: true,
1833
- sample_values: ["Low", "Medium", "High"],
1834
- });
1835
- } else if (dbType === "clickhouse") {
1836
- // ClickHouse: quantile function for tercile bucketing
1837
- virtualDims.push({
1838
- name: `${col.name}_bucket`,
1839
- sql: `CASE\n WHEN ${col.name} < (SELECT quantile(0.33)(${col.name}) FROM ${qualifiedTable}) THEN 'Low'\n WHEN ${col.name} < (SELECT quantile(0.66)(${col.name}) FROM ${qualifiedTable}) THEN 'Medium'\n ELSE 'High'\nEND`,
1840
- type: "string",
1841
- description: `${label} bucketed into Low/Medium/High terciles`,
1842
- virtual: true,
1843
- sample_values: ["Low", "Medium", "High"],
1844
- });
1845
- } else {
1846
- virtualDims.push({
1847
- name: `${col.name}_bucket`,
1848
- sql: `CASE\n WHEN ${col.name} < (SELECT PERCENTILE_CONT(0.33) WITHIN GROUP (ORDER BY ${col.name}) FROM ${qualifiedTable}) THEN 'Low'\n WHEN ${col.name} < (SELECT PERCENTILE_CONT(0.66) WITHIN GROUP (ORDER BY ${col.name}) FROM ${qualifiedTable}) THEN 'Medium'\n ELSE 'High'\nEND`,
1849
- type: "string",
1850
- description: `${label} bucketed into Low/Medium/High terciles`,
1851
- virtual: true,
1852
- sample_values: ["Low", "Medium", "High"],
1853
- });
1854
- }
1855
- }
1856
-
1857
- if (mappedType === "date") {
1858
- if (dbType === "mysql") {
1859
- virtualDims.push({
1860
- name: `${col.name}_year`,
1861
- sql: `YEAR(${col.name})`,
1862
- type: "number",
1863
- description: `Year extracted from ${col.name}`,
1864
- virtual: true,
1865
- });
1866
- virtualDims.push({
1867
- name: `${col.name}_month`,
1868
- sql: `DATE_FORMAT(${col.name}, '%Y-%m')`,
1869
- type: "string",
1870
- description: `Year-month extracted from ${col.name}`,
1871
- virtual: true,
1872
- });
1873
- } else if (dbType === "clickhouse") {
1874
- virtualDims.push({
1875
- name: `${col.name}_year`,
1876
- sql: `toYear(${col.name})`,
1877
- type: "number",
1878
- description: `Year extracted from ${col.name}`,
1879
- virtual: true,
1880
- });
1881
- virtualDims.push({
1882
- name: `${col.name}_month`,
1883
- sql: `formatDateTime(${col.name}, '%Y-%m')`,
1884
- type: "string",
1885
- description: `Year-month extracted from ${col.name}`,
1886
- virtual: true,
1887
- });
1888
- } else if (dbType === "salesforce") {
1889
- virtualDims.push({
1890
- name: `${col.name}_year`,
1891
- sql: `CALENDAR_YEAR(${col.name})`,
1892
- type: "number",
1893
- description: `Year extracted from ${col.name}`,
1894
- virtual: true,
1895
- });
1896
- virtualDims.push({
1897
- name: `${col.name}_month`,
1898
- sql: `CALENDAR_MONTH(${col.name})`,
1899
- type: "number",
1900
- description: `Month extracted from ${col.name}`,
1901
- virtual: true,
1902
- });
1903
- } else {
1904
- virtualDims.push({
1905
- name: `${col.name}_year`,
1906
- sql: `EXTRACT(YEAR FROM ${col.name})`,
1907
- type: "number",
1908
- description: `Year extracted from ${col.name}`,
1909
- virtual: true,
1910
- });
1911
- virtualDims.push({
1912
- name: `${col.name}_month`,
1913
- sql: `TO_CHAR(${col.name}, 'YYYY-MM')`,
1914
- type: "string",
1915
- description: `Year-month extracted from ${col.name}`,
1916
- virtual: true,
1917
- });
1918
- }
1919
- }
1920
- }
1921
-
1922
- // Emit profiler_notes on dimensions
1923
- for (const dim of dimensions) {
1924
- const col = profile.columns.find((c) => c.name === dim.name);
1925
- if (col?.profiler_notes && col.profiler_notes.length > 0) {
1926
- dim.profiler_notes = col.profiler_notes;
1927
- }
1928
- }
1929
-
1930
- // Build joins from constraint FKs
1931
- const joins: Record<string, unknown>[] = profile.foreign_keys.map((fk) => ({
1932
- target_entity: entityName(fk.to_table),
1933
- relationship: "many_to_one",
1934
- join_columns: {
1935
- from: fk.from_column,
1936
- to: fk.to_column,
1937
- },
1938
- description: `Each ${singularize(profile.table_name)} belongs to one ${singularize(fk.to_table)}`,
1939
- }));
1940
-
1941
- // Add inferred joins
1942
- for (const fk of profile.inferred_foreign_keys) {
1943
- joins.push({
1944
- target_entity: entityName(fk.to_table),
1945
- relationship: "many_to_one",
1946
- join_columns: {
1947
- from: fk.from_column,
1948
- to: fk.to_column,
1949
- },
1950
- inferred: true,
1951
- note: `No FK constraint exists — inferred from column name ${fk.from_column}`,
1952
- description: `Each ${singularize(profile.table_name)} likely belongs to one ${singularize(fk.to_table)}`,
1953
- });
1954
- }
1955
-
1956
- // Build measures (skip for views/matviews — they are pre-aggregated or derived; measures should reference source tables instead)
1957
- const measures: Record<string, unknown>[] = [];
1958
-
1959
- if (!isViewLike(profile)) {
1960
- // count_distinct on PK
1961
- const pkCol = profile.columns.find((c) => c.is_primary_key);
1962
- if (pkCol) {
1963
- measures.push({
1964
- name: `${singularize(profile.table_name)}_count`,
1965
- sql: pkCol.name,
1966
- type: "count_distinct",
1967
- });
1968
- }
1969
-
1970
- // sum/avg on numeric non-FK non-PK columns
1971
- for (const col of profile.columns) {
1972
- if (col.is_primary_key || col.is_foreign_key) continue;
1973
- if (col.name.endsWith("_id")) continue;
1974
- const mappedType = mapSQLType(col.type);
1975
- if (mappedType !== "number") continue;
1976
-
1977
- measures.push({
1978
- name: `total_${col.name}`,
1979
- sql: col.name,
1980
- type: "sum",
1981
- description: `Sum of ${col.name.replace(/_/g, " ")}`,
1982
- });
1983
- measures.push({
1984
- name: `avg_${col.name}`,
1985
- sql: col.name,
1986
- type: "avg",
1987
- description: `Average ${col.name.replace(/_/g, " ")}`,
1988
- });
1989
- }
1990
- }
1991
-
1992
- // Build use_cases
1993
- const useCases: string[] = [];
1994
-
1995
- // Note for views
1996
- if (isView(profile)) {
1997
- useCases.push(`This is a database view — it may encapsulate complex joins or aggregations. Query it directly rather than recreating its logic`);
1998
- }
1999
-
2000
- // Notes for materialized views
2001
- if (isMatView(profile)) {
2002
- useCases.push(`WARNING: This is a materialized view — data may be stale. Check with the user about refresh frequency before relying on real-time accuracy`);
2003
- if (profile.matview_populated === false) {
2004
- useCases.push(`WARNING: This materialized view has never been refreshed and contains no data`);
2005
- }
2006
- }
2007
-
2008
- // Note for partitioned tables
2009
- if (profile.partition_info) {
2010
- useCases.push(`This table is partitioned by ${profile.partition_info.strategy} on (${profile.partition_info.key}). Always include ${profile.partition_info.key} in WHERE clauses for optimal query performance`);
2011
- }
2012
-
2013
- // Prepend warnings for flagged tables
2014
- if (profile.table_flags.possibly_abandoned) {
2015
- useCases.push(`WARNING: This table appears to be abandoned/legacy. Verify with the user before querying`);
2016
- }
2017
- if (profile.table_flags.possibly_denormalized) {
2018
- useCases.push(`WARNING: This is a denormalized/materialized table. Data may be stale or duplicate other tables`);
2019
- }
2020
-
2021
- const enumCols = profile.columns.filter((c) => c.is_enum_like);
2022
- const numericCols = profile.columns.filter(
2023
- (c) =>
2024
- mapSQLType(c.type) === "number" && !c.is_primary_key && !c.is_foreign_key && !c.name.endsWith("_id")
2025
- );
2026
- const dateCols = profile.columns.filter(
2027
- (c) => mapSQLType(c.type) === "date"
2028
- );
2029
-
2030
- if (enumCols.length > 0)
2031
- useCases.push(
2032
- `Use for segmentation analysis by ${enumCols.map((c) => c.name).join(", ")}`
2033
- );
2034
- if (numericCols.length > 0)
2035
- useCases.push(
2036
- `Use for aggregation and trends on ${numericCols.map((c) => c.name).join(", ")}`
2037
- );
2038
- if (dateCols.length > 0)
2039
- useCases.push(`Use for time-series analysis using ${dateCols.map((c) => c.name).join(", ")}`);
2040
-
2041
- // Combined FK list for use_cases
2042
- const allFKs = [...profile.foreign_keys, ...profile.inferred_foreign_keys];
2043
- if (joins.length > 0) {
2044
- const targets = allFKs.map((fk) => fk.to_table);
2045
- const uniqueTargets = [...new Set(targets)];
2046
- useCases.push(
2047
- `Join with ${uniqueTargets.join(", ")} for cross-entity analysis`
2048
- );
2049
- }
2050
- // Add "avoid" guidance for related tables (constraint + inferred)
2051
- const tablesPointingHere = allProfiles.filter((p) =>
2052
- [...p.foreign_keys, ...p.inferred_foreign_keys].some((fk) => fk.to_table === profile.table_name)
2053
- );
2054
- if (tablesPointingHere.length > 0) {
2055
- useCases.push(
2056
- `Avoid for row-level ${tablesPointingHere.map((p) => p.table_name).join("/")} queries — use those entities directly`
2057
- );
2058
- }
2059
- if (useCases.length === 0) {
2060
- useCases.push(`Use for querying ${profile.table_name} data`);
2061
- }
2062
-
2063
- // Build query patterns (skip for views/matviews — the view IS the pattern)
2064
- const queryPatterns: Record<string, unknown>[] = [];
2065
-
2066
- if (!isViewLike(profile)) {
2067
- // Pattern: count by enum column
2068
- for (const col of enumCols.slice(0, 2)) {
2069
- queryPatterns.push({
2070
- description: `${entityName(profile.table_name)} by ${col.name}`,
2071
- sql: `SELECT ${col.name}, COUNT(*) as count\nFROM ${qualifiedTable}\nGROUP BY ${col.name}\nORDER BY count DESC`,
2072
- });
2073
- }
2074
-
2075
- // Pattern: aggregate numeric by enum
2076
- if (numericCols.length > 0 && enumCols.length > 0) {
2077
- const numCol = numericCols[0];
2078
- const enumCol = enumCols[0];
2079
- queryPatterns.push({
2080
- description: `Total ${numCol.name} by ${enumCol.name}`,
2081
- sql: `SELECT ${enumCol.name}, SUM(${numCol.name}) as total_${numCol.name}, COUNT(*) as count\nFROM ${qualifiedTable}\nGROUP BY ${enumCol.name}\nORDER BY total_${numCol.name} DESC`,
2082
- });
2083
- }
2084
- }
2085
-
2086
- // Build description with optional suffix for flagged tables
2087
- const profileIsViewLike = isViewLike(profile);
2088
- const profileIsMatView = isMatView(profile);
2089
- let description: string;
2090
- if (profileIsMatView) {
2091
- description = `Materialized view: ${profile.table_name} (${profile.row_count.toLocaleString()} rows). Contains ${profile.columns.length} columns.`;
2092
- } else if (isView(profile)) {
2093
- description = `Database view: ${profile.table_name} (${profile.row_count.toLocaleString()} rows). Contains ${profile.columns.length} columns.`;
2094
- } else {
2095
- description = `Auto-profiled schema for ${profile.table_name} (${profile.row_count.toLocaleString()} rows). Contains ${profile.columns.length} columns${allFKs.length > 0 ? `, linked to ${[...new Set(allFKs.map((fk) => fk.to_table))].join(", ")}` : ""}.`;
2096
- }
2097
- if (profile.table_flags.possibly_abandoned) {
2098
- description += ` POSSIBLY ABANDONED — name matches legacy/temp pattern and no tables reference it.`;
2099
- }
2100
- if (profile.table_flags.possibly_denormalized) {
2101
- description += ` DENORMALIZED — data may duplicate other tables.`;
2102
- }
2103
-
2104
- // Determine entity type
2105
- let entityType: string;
2106
- if (profileIsMatView) {
2107
- entityType = "materialized_view";
2108
- } else if (isView(profile)) {
2109
- entityType = "view";
2110
- } else {
2111
- entityType = "fact_table";
2112
- }
2113
-
2114
- // Assemble entity
2115
- const entity: Record<string, unknown> = {
2116
- name,
2117
- type: entityType,
2118
- table: qualifiedTable,
2119
- ...(source ? { connection: source } : {}),
2120
- grain: profileIsMatView
2121
- ? `one row per result from ${profile.table_name} materialized view`
2122
- : profileIsViewLike
2123
- ? `one row per result from ${profile.table_name} view`
2124
- : `one row per ${singularize(profile.table_name).replace(/_/g, " ")} record`,
2125
- description,
2126
- dimensions: [...dimensions, ...virtualDims],
2127
- };
2128
-
2129
- // Partition metadata
2130
- if (profile.partition_info) {
2131
- entity.partitioned = true;
2132
- entity.partition_strategy = profile.partition_info.strategy;
2133
- entity.partition_key = profile.partition_info.key;
2134
- }
2135
-
2136
- if (measures.length > 0) entity.measures = measures;
2137
- if (joins.length > 0) entity.joins = joins;
2138
- entity.use_cases = useCases;
2139
- if (queryPatterns.length > 0) entity.query_patterns = queryPatterns;
2140
-
2141
- // Emit table-level profiler notes
2142
- if (profile.profiler_notes.length > 0) {
2143
- entity.profiler_notes = profile.profiler_notes;
2144
- }
2145
-
2146
- return yaml.dump(entity, { lineWidth: 120, noRefs: true });
2147
- }
2148
-
2149
- export function generateCatalogYAML(profiles: TableProfile[]): string {
2150
- const catalog: Record<string, unknown> = {
2151
- version: "1.0",
2152
- entities: profiles.map((p) => {
2153
- const enumCols = p.columns.filter((c) => c.is_enum_like);
2154
- const numericCols = p.columns.filter(
2155
- (c) =>
2156
- mapSQLType(c.type) === "number" && !c.is_primary_key && !c.is_foreign_key && !c.name.endsWith("_id")
2157
- );
2158
-
2159
- // Generate use_for from table characteristics
2160
- const useFor: string[] = [];
2161
- if (enumCols.length > 0) {
2162
- useFor.push(
2163
- `Segmentation by ${enumCols.map((c) => c.name).join(", ")}`
2164
- );
2165
- }
2166
- if (numericCols.length > 0) {
2167
- useFor.push(
2168
- `Aggregation on ${numericCols.map((c) => c.name).join(", ")}`
2169
- );
2170
- }
2171
- const allFKs = [...p.foreign_keys, ...p.inferred_foreign_keys];
2172
- if (allFKs.length > 0) {
2173
- useFor.push(
2174
- `Cross-entity analysis via ${[...new Set(allFKs.map((fk) => fk.to_table))].join(", ")}`
2175
- );
2176
- }
2177
- if (useFor.length === 0) {
2178
- useFor.push(`General queries on ${p.table_name}`);
2179
- }
2180
-
2181
- // Generate common_questions from column types
2182
- const questions: string[] = [];
2183
- for (const col of enumCols.slice(0, 2)) {
2184
- questions.push(
2185
- `How many ${p.table_name} by ${col.name}?`
2186
- );
2187
- }
2188
- if (numericCols.length > 0) {
2189
- questions.push(
2190
- `What is the average ${numericCols[0].name} across ${p.table_name}?`
2191
- );
2192
- }
2193
- if (allFKs.length > 0) {
2194
- const fk = allFKs[0];
2195
- questions.push(
2196
- `How are ${p.table_name} distributed across ${fk.to_table}?`
2197
- );
2198
- }
2199
- if (questions.length === 0) {
2200
- questions.push(`What data is in ${p.table_name}?`);
2201
- }
2202
-
2203
- const entryIsMatView = isMatView(p);
2204
- const entryIsViewLike = isViewLike(p);
2205
-
2206
- let catalogDesc: string;
2207
- if (entryIsMatView) {
2208
- catalogDesc = `${p.table_name} [materialized view] (${p.row_count.toLocaleString()} rows, ${p.columns.length} columns)`;
2209
- } else if (isView(p)) {
2210
- catalogDesc = `${p.table_name} [view] (${p.row_count.toLocaleString()} rows, ${p.columns.length} columns)`;
2211
- } else {
2212
- catalogDesc = `${p.table_name} (${p.row_count.toLocaleString()} rows, ${p.columns.length} columns)`;
2213
- }
2214
- if (p.partition_info) {
2215
- catalogDesc += ` [partitioned by ${p.partition_info.strategy}]`;
2216
- }
2217
-
2218
- return {
2219
- name: entityName(p.table_name),
2220
- file: `entities/${p.table_name}.yml`,
2221
- grain: entryIsMatView
2222
- ? `one row per result from ${p.table_name} materialized view`
2223
- : entryIsViewLike
2224
- ? `one row per result from ${p.table_name} view`
2225
- : `one row per ${singularize(p.table_name).replace(/_/g, " ")} record`,
2226
- description: catalogDesc,
2227
- use_for: useFor,
2228
- common_questions: questions,
2229
- };
2230
- }),
2231
- glossary: "glossary.yml",
2232
- };
2233
-
2234
- // Add metrics section if we'll be generating metric files (exclude views/matviews)
2235
- const tablesWithNumericCols = profiles.filter((p) =>
2236
- !isViewLike(p) &&
2237
- p.columns.some(
2238
- (c) =>
2239
- mapSQLType(c.type) === "number" && !c.is_primary_key && !c.is_foreign_key && !c.name.endsWith("_id")
2240
- )
2241
- );
2242
- if (tablesWithNumericCols.length > 0) {
2243
- catalog.metrics = tablesWithNumericCols.map((p) => ({
2244
- file: `metrics/${p.table_name}.yml`,
2245
- description: `Auto-generated metrics for ${p.table_name}`,
2246
- }));
2247
- }
2248
-
2249
- // Add tech_debt section for flagged tables
2250
- const flaggedTables: { table: string; issues: string[] }[] = [];
2251
- for (const p of profiles) {
2252
- const issues: string[] = [];
2253
- if (p.table_flags.possibly_abandoned) issues.push("possibly_abandoned");
2254
- if (p.table_flags.possibly_denormalized) issues.push("possibly_denormalized");
2255
- if (p.inferred_foreign_keys.length > 0) issues.push("missing_fk_constraints");
2256
- const hasEnumIssues = p.columns.some((c) =>
2257
- c.profiler_notes.some((n) => n.startsWith("Case-inconsistent"))
2258
- );
2259
- if (hasEnumIssues) issues.push("inconsistent_enums");
2260
- if (issues.length > 0) flaggedTables.push({ table: p.table_name, issues });
2261
- }
2262
- if (flaggedTables.length > 0) {
2263
- catalog.tech_debt = flaggedTables;
2264
- }
2265
-
2266
- return yaml.dump(catalog, { lineWidth: 120, noRefs: true });
2267
- }
2268
-
2269
- export function generateMetricYAML(profile: TableProfile, schema: string = "public"): string | null {
2270
- if (isViewLike(profile)) return null;
2271
-
2272
- const numericCols = profile.columns.filter(
2273
- (c) =>
2274
- mapSQLType(c.type) === "number" &&
2275
- !c.is_primary_key &&
2276
- !c.is_foreign_key &&
2277
- !c.name.endsWith("_id")
2278
- );
2279
-
2280
- if (numericCols.length === 0) return null;
2281
-
2282
- const pkCol = profile.columns.find((c) => c.is_primary_key);
2283
- const enumCols = profile.columns.filter((c) => c.is_enum_like);
2284
- const qualifiedTable = schema !== "public" ? `${schema}.${profile.table_name}` : profile.table_name;
2285
-
2286
- const metrics: Record<string, unknown>[] = [];
2287
-
2288
- // Count metric
2289
- if (pkCol) {
2290
- metrics.push({
2291
- id: `${profile.table_name}_count`,
2292
- label: `Total ${entityName(profile.table_name)}`,
2293
- description: `Count of distinct ${profile.table_name} records.`,
2294
- type: "atomic",
2295
- sql: `SELECT COUNT(DISTINCT ${pkCol.name}) as count\nFROM ${qualifiedTable}`,
2296
- aggregation: "count_distinct",
2297
- });
2298
- }
2299
-
2300
- // Sum and average for each numeric column
2301
- for (const col of numericCols) {
2302
- metrics.push({
2303
- id: `total_${col.name}`,
2304
- label: `Total ${col.name.replace(/_/g, " ")}`,
2305
- description: `Sum of ${col.name} across all ${profile.table_name}.`,
2306
- type: "atomic",
2307
- source: {
2308
- entity: entityName(profile.table_name),
2309
- measure: `total_${col.name}`,
2310
- },
2311
- sql: `SELECT SUM(${col.name}) as total_${col.name}\nFROM ${qualifiedTable}`,
2312
- aggregation: "sum",
2313
- objective: "maximize",
2314
- });
2315
-
2316
- metrics.push({
2317
- id: `avg_${col.name}`,
2318
- label: `Average ${col.name.replace(/_/g, " ")}`,
2319
- description: `Average ${col.name} per ${singularize(profile.table_name)}.`,
2320
- type: "atomic",
2321
- sql: `SELECT AVG(${col.name}) as avg_${col.name}\nFROM ${qualifiedTable}`,
2322
- aggregation: "avg",
2323
- });
2324
-
2325
- // Breakdown by first enum column if available
2326
- if (enumCols.length > 0) {
2327
- const enumCol = enumCols[0];
2328
- metrics.push({
2329
- id: `${col.name}_by_${enumCol.name}`,
2330
- label: `${col.name.replace(/_/g, " ")} by ${enumCol.name}`,
2331
- description: `${col.name} broken down by ${enumCol.name}.`,
2332
- type: "atomic",
2333
- sql: `SELECT ${enumCol.name}, SUM(${col.name}) as total_${col.name}, AVG(${col.name}) as avg_${col.name}, COUNT(*) as count\nFROM ${qualifiedTable}\nGROUP BY ${enumCol.name}\nORDER BY total_${col.name} DESC`,
2334
- });
2335
- }
2336
- }
2337
-
2338
- return yaml.dump({ metrics }, { lineWidth: 120, noRefs: true });
2339
- }
2340
-
2341
- export function generateGlossaryYAML(profiles: TableProfile[]): string {
2342
- const terms: Record<string, unknown> = {};
2343
-
2344
- // Find columns that appear in multiple tables (ambiguous terms)
2345
- const columnToTables = new Map<string, string[]>();
2346
- for (const p of profiles) {
2347
- for (const col of p.columns) {
2348
- if (col.is_primary_key || col.is_foreign_key) continue;
2349
- const existing = columnToTables.get(col.name) ?? [];
2350
- existing.push(p.table_name);
2351
- columnToTables.set(col.name, existing);
2352
- }
2353
- }
2354
-
2355
- for (const [colName, tables] of columnToTables) {
2356
- if (tables.length > 1) {
2357
- terms[colName] = {
2358
- status: "ambiguous",
2359
- note: `"${colName}" appears in multiple tables: ${tables.join(", ")}. ASK the user which table they mean.`,
2360
- possible_mappings: tables.map((t) => `${t}.${colName}`),
2361
- };
2362
- }
2363
- }
2364
-
2365
- // Add FK relationship terms
2366
- for (const p of profiles) {
2367
- for (const fk of p.foreign_keys) {
2368
- const termName = fk.from_column.replace(/_id$/, "");
2369
- if (!terms[termName]) {
2370
- terms[termName] = {
2371
- status: "defined",
2372
- definition: `Refers to the ${fk.to_table} entity. Linked via ${p.table_name}.${fk.from_column} → ${fk.to_table}.${fk.to_column}.`,
2373
- };
2374
- }
2375
- }
2376
- }
891
+ is_foreign_key: isFK,
892
+ fk_target_table: isFK ? field.referenceTo[0] : null,
893
+ fk_target_column: isFK ? "Id" : null,
894
+ is_enum_like: isEnumLike,
895
+ profiler_notes: [],
896
+ };
897
+ });
2377
898
 
2378
- // Add enum-like column terms
2379
- for (const p of profiles) {
2380
- for (const col of p.columns) {
2381
- if (col.is_enum_like && !terms[col.name]) {
2382
- terms[col.name] = {
2383
- status: "defined",
2384
- definition: `Categorical field on ${p.table_name}. Possible values: ${col.sample_values.join(", ")}.`,
2385
- };
899
+ profiles.push({
900
+ table_name: objectName,
901
+ object_type: "table",
902
+ row_count: rowCount,
903
+ columns,
904
+ primary_key_columns: primaryKeyColumns,
905
+ foreign_keys: foreignKeys,
906
+ inferred_foreign_keys: [],
907
+ profiler_notes: [],
908
+ table_flags: { possibly_abandoned: false, possibly_denormalized: false },
909
+ });
910
+ progress?.onTableDone(objectName, i, objectsToProfile.length);
911
+ } catch (err) {
912
+ const msg = err instanceof Error ? err.message : String(err);
913
+ // Fail fast on connection-level errors that will affect all remaining objects
914
+ if (isFatalConnectionError(err)) {
915
+ throw new Error(`Fatal Salesforce error while profiling ${objectName}: ${msg}`, { cause: err });
916
+ }
917
+ if (progress) {
918
+ progress.onTableError(objectName, msg, i, objectsToProfile.length);
919
+ } else {
920
+ console.error(` Warning: Failed to profile ${objectName}: ${msg}`);
921
+ }
922
+ errors.push({ table: objectName, error: msg });
923
+ continue;
2386
924
  }
2387
925
  }
926
+ } finally {
927
+ await source.close();
2388
928
  }
2389
929
 
2390
- // Add ambiguous terms for columns with case-inconsistent enums
2391
- for (const p of profiles) {
2392
- for (const col of p.columns) {
2393
- if (!col.is_enum_like) continue;
2394
- const inconsistencyNote = col.profiler_notes.find((n) =>
2395
- n.startsWith("Case-inconsistent")
2396
- );
2397
- if (!inconsistencyNote) continue;
2398
-
2399
- const termKey = `${p.table_name}.${col.name}`;
2400
- terms[termKey] = {
2401
- status: "ambiguous",
2402
- note: `${col.name} on ${p.table_name} has case-inconsistent values. Use LOWER(${col.name}) when grouping or filtering.`,
2403
- guidance: `Always wrap in LOWER() for reliable aggregation: GROUP BY LOWER(${col.name})`,
2404
- };
2405
- }
2406
- }
2407
-
2408
- if (Object.keys(terms).length === 0) {
2409
- terms["example_term"] = {
2410
- status: "defined",
2411
- definition: "Replace this with your own business terms",
2412
- };
2413
- }
2414
-
2415
- return yaml.dump({ terms }, { lineWidth: 120, noRefs: true });
930
+ return { profiles, errors };
2416
931
  }
2417
932
 
2418
- export function mapSQLType(sqlType: string): string {
2419
- // Strip ClickHouse wrappers (Nullable, LowCardinality) before matching
2420
- const unwrapped = sqlType.replace(/Nullable\((.+)\)/g, "$1").replace(/LowCardinality\((.+)\)/g, "$1");
2421
- const t = unwrapped.toLowerCase();
2422
- // interval and money look like they contain "int" — handle before the numeric check
2423
- if (t.includes("interval") || t.includes("money")) return "string";
2424
- if (
2425
- t.includes("int") ||
2426
- t.includes("float") ||
2427
- t.includes("real") ||
2428
- t.includes("numeric") ||
2429
- t.includes("decimal") ||
2430
- t.includes("double") ||
2431
- t === "currency" ||
2432
- t === "percent" ||
2433
- t === "long"
2434
- )
2435
- return "number";
2436
- if (t.startsWith("bool")) return "boolean";
2437
- if (t.includes("date") || t.includes("time") || t.includes("timestamp"))
2438
- return "date";
2439
- return "string";
2440
- }
2441
933
 
2442
934
  // --- DuckDB profiler (CSV/Parquet files) ---
2443
935
 
2444
936
  /** Helper to run a DuckDB query and return typed rows. */
2445
937
  async function duckdbQuery<T = Record<string, unknown>>(
2446
- conn: unknown,
938
+ conn: DuckDBConnection,
2447
939
  sql: string,
2448
940
  ): Promise<T[]> {
2449
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2450
- const reader = await (conn as any).runAndReadAll(sql);
941
+ const reader = await conn.runAndReadAll(sql);
2451
942
  return reader.getRowObjects() as T[];
2452
943
  }
2453
944
 
@@ -2488,18 +979,15 @@ export async function ingestIntoDuckDB(
2488
979
  ? `read_csv_auto('${absPath.replace(/'/g, "''")}')`
2489
980
  : `read_parquet('${absPath.replace(/'/g, "''")}')`
2490
981
 
2491
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2492
- await (conn as any).run(`CREATE TABLE "${stem}" AS SELECT * FROM ${readFn}`);
982
+ await conn.run(`CREATE TABLE "${stem}" AS SELECT * FROM ${readFn}`);
2493
983
  tableNames.push(stem);
2494
984
  console.log(` Loaded ${file.format.toUpperCase()} → table "${stem}" from ${file.path}`);
2495
985
  }
2496
986
  return tableNames;
2497
987
  } finally {
2498
988
  // DuckDB Neo API uses synchronous cleanup methods
2499
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2500
- (conn as any).disconnectSync();
2501
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2502
- (instance as any).closeSync();
989
+ conn.disconnectSync();
990
+ instance.closeSync();
2503
991
  }
2504
992
  }
2505
993
 
@@ -2523,10 +1011,8 @@ export async function listDuckDBObjects(dbPath: string): Promise<DatabaseObject[
2523
1011
  }));
2524
1012
  } finally {
2525
1013
  // DuckDB Neo API uses synchronous cleanup methods
2526
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2527
- (conn as any).disconnectSync();
2528
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2529
- (instance as any).closeSync();
1014
+ conn.disconnectSync();
1015
+ instance.closeSync();
2530
1016
  }
2531
1017
  }
2532
1018
 
@@ -2548,11 +1034,13 @@ export async function profileDuckDB(
2548
1034
  dbPath: string,
2549
1035
  filterTables?: string[],
2550
1036
  prefetchedObjects?: DatabaseObject[],
2551
- ): Promise<TableProfile[]> {
1037
+ progress?: ProfileProgressCallbacks
1038
+ ): Promise<ProfilingResult> {
2552
1039
  const DuckDBInstance = await loadDuckDB();
2553
1040
  const instance = await DuckDBInstance.create(dbPath, { access_mode: "READ_ONLY" });
2554
1041
  const conn = await instance.connect();
2555
1042
  const profiles: TableProfile[] = [];
1043
+ const errors: ProfileError[] = [];
2556
1044
 
2557
1045
  try {
2558
1046
  let allObjects: DatabaseObject[];
@@ -2566,10 +1054,17 @@ export async function profileDuckDB(
2566
1054
  ? allObjects.filter((o) => filterTables.includes(o.name))
2567
1055
  : allObjects;
2568
1056
 
1057
+ progress?.onStart(objectsToProfile.length);
1058
+
2569
1059
  for (const [i, obj] of objectsToProfile.entries()) {
2570
1060
  const tableName = obj.name;
2571
1061
  const objectType = obj.type;
2572
- console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${tableName}${objectType === "view" ? " [view]" : ""}...`);
1062
+ const objectLabel = objectType === "view" ? " [view]" : "";
1063
+ if (progress) {
1064
+ progress.onTableStart(tableName + objectLabel, i, objectsToProfile.length);
1065
+ } else {
1066
+ console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${tableName}${objectLabel}...`);
1067
+ }
2573
1068
 
2574
1069
  try {
2575
1070
  const countRows = await duckdbQuery<{ c: number | bigint }>(conn, `SELECT COUNT(*) as c FROM "${tableName}"`);
@@ -2622,6 +1117,7 @@ export async function profileDuckDB(
2622
1117
  sampleValues = sampleRows.map((r) => String(r.v));
2623
1118
  }
2624
1119
  } catch (colErr) {
1120
+ if (isFatalConnectionError(colErr)) throw colErr;
2625
1121
  console.warn(
2626
1122
  ` Warning: Could not profile column ${tableName}.${col.column_name}: ${colErr instanceof Error ? colErr.message : String(colErr)}`
2627
1123
  );
@@ -2657,19 +1153,28 @@ export async function profileDuckDB(
2657
1153
  possibly_denormalized: false,
2658
1154
  },
2659
1155
  });
1156
+ progress?.onTableDone(tableName, i, objectsToProfile.length);
2660
1157
  } catch (err) {
2661
- console.warn(` Warning: Failed to profile ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
1158
+ const msg = err instanceof Error ? err.message : String(err);
1159
+ // Fail fast on connection-level errors that will affect all remaining tables
1160
+ if (isFatalConnectionError(err)) {
1161
+ throw new Error(`Fatal database error while profiling ${tableName}: ${msg}`, { cause: err });
1162
+ }
1163
+ if (progress) {
1164
+ progress.onTableError(tableName, msg, i, objectsToProfile.length);
1165
+ } else {
1166
+ console.error(` Warning: Failed to profile ${tableName}: ${msg}`);
1167
+ }
1168
+ errors.push({ table: tableName, error: msg });
2662
1169
  }
2663
1170
  }
2664
1171
  } finally {
2665
1172
  // DuckDB Neo API uses synchronous cleanup methods
2666
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2667
- (conn as any).disconnectSync();
2668
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2669
- (instance as any).closeSync();
1173
+ conn.disconnectSync();
1174
+ instance.closeSync();
2670
1175
  }
2671
1176
 
2672
- return profiles;
1177
+ return { profiles, errors };
2673
1178
  }
2674
1179
 
2675
1180
  // --- Demo datasets ---
@@ -2916,7 +1421,13 @@ export function computeDiff(
2916
1421
 
2917
1422
  // Metadata differences
2918
1423
  const metadataChanges: string[] = [];
2919
- if (db.objectType && yml.objectType && db.objectType !== yml.objectType) {
1424
+ // Only flag type changes that indicate real schema drift (e.g. table↔view).
1425
+ // Semantic classifications like dimension_table vs fact_table are enrichment
1426
+ // metadata — the profiler always assigns "fact_table" to non-views, so comparing
1427
+ // it against enriched YAML produces false positives.
1428
+ const semanticTypes = new Set(["fact_table", "dimension_table"]);
1429
+ if (db.objectType && yml.objectType && db.objectType !== yml.objectType
1430
+ && !(semanticTypes.has(db.objectType) && semanticTypes.has(yml.objectType))) {
2920
1431
  metadataChanges.push(`type changed: ${yml.objectType} → ${db.objectType}`);
2921
1432
  }
2922
1433
  if (db.partitionStrategy !== yml.partitionStrategy) {
@@ -3147,7 +1658,10 @@ export async function handleActionApproval(
3147
1658
  signal: AbortSignal.timeout(30_000),
3148
1659
  });
3149
1660
  if (!res.ok) {
3150
- const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
1661
+ const body = (await res.json().catch(() => {
1662
+ // intentionally ignored: error response may not be JSON; fall back to status code
1663
+ return {};
1664
+ })) as Record<string, unknown>;
3151
1665
  return { ok: false, error: (body.message as string) ?? `HTTP ${res.status}` };
3152
1666
  }
3153
1667
  const body = (await res.json()) as Record<string, unknown>;
@@ -3834,6 +2348,183 @@ async function handleQuery(args: string[]): Promise<void> {
3834
2348
  }
3835
2349
  }
3836
2350
 
2351
+ // --- Index CLI handler ---
2352
+
2353
+ async function handleIndex(args: string[]): Promise<void> {
2354
+ const statsOnly = args.includes("--stats");
2355
+
2356
+ if (!fs.existsSync(SEMANTIC_DIR)) {
2357
+ console.error(pc.red("No semantic/ directory found. Run 'atlas init' first."));
2358
+ process.exit(1);
2359
+ }
2360
+
2361
+ try {
2362
+ const { getSemanticIndexStats, buildSemanticIndex } = await import("@atlas/api/lib/semantic/search");
2363
+
2364
+ // Use stats-based validation — works for both default and per-source layouts
2365
+ const stats = getSemanticIndexStats(SEMANTIC_DIR);
2366
+
2367
+ if (stats.entities === 0) {
2368
+ console.error(pc.red("No valid entity YAML files found in semantic/. Run 'atlas init' first."));
2369
+ process.exit(1);
2370
+ }
2371
+
2372
+ if (statsOnly) {
2373
+ console.log(
2374
+ `${pc.bold("Semantic index stats:")} ` +
2375
+ `${stats.entities} entities, ${stats.dimensions} dimensions, ` +
2376
+ `${stats.measures} measures, ${stats.metrics} metrics, ` +
2377
+ `${stats.glossaryTerms} glossary terms (${stats.keywords} keywords)`
2378
+ );
2379
+ return;
2380
+ }
2381
+
2382
+ // Full rebuild — buildSemanticIndex does its own loading; stats above are for validation + display
2383
+ const start = Date.now();
2384
+ buildSemanticIndex(SEMANTIC_DIR);
2385
+ const elapsed = Date.now() - start;
2386
+
2387
+ console.log(
2388
+ pc.green("✓") + ` Indexed ${stats.entities} entities, ` +
2389
+ `${stats.dimensions} dimensions, ${stats.measures} measures ` +
2390
+ `(${stats.keywords} keywords) in ${elapsed}ms`
2391
+ );
2392
+ } catch (err) {
2393
+ console.error(pc.red("Failed to build semantic index."));
2394
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
2395
+ process.exit(1);
2396
+ }
2397
+ }
2398
+
2399
+ // --- Learn CLI handler ---
2400
+
2401
+ async function handleLearn(args: string[]): Promise<void> {
2402
+ const applyMode = args.includes("--apply");
2403
+ const runSuggestions = args.includes("--suggestions");
2404
+ const limitArg = getFlag(args, "--limit");
2405
+ const sinceArg = getFlag(args, "--since");
2406
+ const sourceArg = requireFlagIdentifier(args, "--source", "source name");
2407
+
2408
+ // Resolve semantic directories
2409
+ const semanticRoot = sourceArg
2410
+ ? path.join(SEMANTIC_DIR, sourceArg)
2411
+ : SEMANTIC_DIR;
2412
+ const entitiesDir = sourceArg
2413
+ ? path.join(semanticRoot, "entities")
2414
+ : ENTITIES_DIR;
2415
+
2416
+ // Validate semantic layer exists
2417
+ if (!fs.existsSync(entitiesDir)) {
2418
+ console.error(pc.red(`No entities found at ${entitiesDir}. Run 'atlas init' first.`));
2419
+ process.exit(1);
2420
+ }
2421
+
2422
+ // Validate internal DB is configured
2423
+ if (!process.env.DATABASE_URL) {
2424
+ console.error(pc.red("DATABASE_URL is required for atlas learn."));
2425
+ console.error(" The audit log is stored in the internal database.");
2426
+ console.error(" Set DATABASE_URL=postgresql://... to enable audit log analysis.");
2427
+ process.exit(1);
2428
+ }
2429
+
2430
+ // Validate --limit
2431
+ const limit = limitArg ? parseInt(limitArg, 10) : 1000;
2432
+ if (Number.isNaN(limit) || limit <= 0) {
2433
+ console.error(pc.red(`Invalid value for --limit: "${limitArg}". Expected a positive integer.`));
2434
+ process.exit(1);
2435
+ }
2436
+
2437
+ // Validate --since
2438
+ if (sinceArg) {
2439
+ const sinceDate = new Date(sinceArg);
2440
+ if (Number.isNaN(sinceDate.getTime())) {
2441
+ console.error(pc.red(`Invalid value for --since: "${sinceArg}". Expected ISO 8601 format (e.g., 2026-03-01).`));
2442
+ process.exit(1);
2443
+ }
2444
+ }
2445
+
2446
+ console.log(`\nAtlas Learn — analyzing audit log for YAML improvements...\n`);
2447
+
2448
+ const { getInternalDB, closeInternalDB } = await import("@atlas/api/lib/db/internal");
2449
+ try {
2450
+ const { fetchAuditLog, analyzeQueries } = await import("../lib/learn/analyze");
2451
+ const { loadEntities, loadGlossary, generateProposals, applyProposals } = await import("../lib/learn/propose");
2452
+ const { formatDiff, formatSummary } = await import("../lib/learn/diff");
2453
+
2454
+ // 1. Fetch audit log
2455
+ const pool = getInternalDB();
2456
+ const rows = await fetchAuditLog(pool, { limit, since: sinceArg });
2457
+
2458
+ if (rows.length === 0) {
2459
+ console.log(pc.yellow("No successful queries found in the audit log."));
2460
+ console.log(" Run some queries first, then try again.");
2461
+ return;
2462
+ }
2463
+
2464
+ console.log(` Analyzed ${pc.bold(String(rows.length))} successful queries`);
2465
+
2466
+ // 2. Analyze patterns
2467
+ const analysis = analyzeQueries(rows);
2468
+ console.log(` Found ${pc.bold(String(analysis.patterns.length))} recurring patterns, ` +
2469
+ `${pc.bold(String(analysis.joins.size))} join pairs, ` +
2470
+ `${pc.bold(String(analysis.aliases.length))} column aliases`);
2471
+
2472
+ // 3. Load existing YAML
2473
+ const entities = loadEntities(entitiesDir);
2474
+ const glossaryData = loadGlossary(semanticRoot);
2475
+
2476
+ if (entities.size === 0) {
2477
+ console.error(pc.red(`No valid entity YAML files found in ${entitiesDir}.`));
2478
+ process.exit(1);
2479
+ }
2480
+
2481
+ console.log(` Comparing against ${pc.bold(String(entities.size))} entities\n`);
2482
+
2483
+ // 4. Generate proposals
2484
+ const proposalSet = generateProposals(analysis, entities, glossaryData);
2485
+
2486
+ // 5. Output results
2487
+ console.log(formatSummary(proposalSet));
2488
+
2489
+ if (proposalSet.proposals.length > 0) {
2490
+ console.log(formatDiff(proposalSet));
2491
+
2492
+ if (applyMode) {
2493
+ const { written, failed } = applyProposals(proposalSet);
2494
+ if (written.length > 0) {
2495
+ console.log(pc.green(`\n✓ Applied changes to ${written.length} file(s):`));
2496
+ for (const f of written) {
2497
+ console.log(` ${f.replace(process.cwd() + "/", "")}`);
2498
+ }
2499
+ }
2500
+ if (failed.length > 0) {
2501
+ console.error(pc.red(`\n✗ Failed to write ${failed.length} file(s):`));
2502
+ for (const f of failed) {
2503
+ console.error(` ${f.path.replace(process.cwd() + "/", "")}: ${f.error}`);
2504
+ }
2505
+ process.exit(1);
2506
+ }
2507
+ } else {
2508
+ console.log(pc.dim("\nDry run — no files modified. Use --apply to write changes."));
2509
+ }
2510
+ }
2511
+
2512
+ if (runSuggestions) {
2513
+ console.log("\n📊 Generating query suggestions from audit log...");
2514
+ const { generateSuggestions } = await import("@atlas/api/lib/learn/suggestions");
2515
+ const result = await generateSuggestions(null); // CLI runs in single-org mode
2516
+ console.log(` Created: ${pc.bold(String(result.created))} suggestions`);
2517
+ console.log(` Updated: ${pc.bold(String(result.updated))} suggestions`);
2518
+ }
2519
+ } catch (err) {
2520
+ console.error(pc.red("Failed to analyze audit log."));
2521
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
2522
+ process.exit(1);
2523
+ } finally {
2524
+ await closeInternalDB();
2525
+ }
2526
+ }
2527
+
3837
2528
  // --- Diff CLI handler ---
3838
2529
 
3839
2530
  async function handleDiff(args: string[]): Promise<void> {
@@ -3927,8 +2618,7 @@ async function handleDiff(args: string[]): Promise<void> {
3927
2618
  } else if (dbType === "salesforce") {
3928
2619
  const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
3929
2620
  const config = parseSalesforceURL(connStr);
3930
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3931
- const source: any = createSalesforceConnection(config);
2621
+ const source = createSalesforceConnection(config);
3932
2622
  try {
3933
2623
  const objects = await source.listObjects();
3934
2624
  console.log(`Connected: Salesforce (${objects.length} queryable objects)`);
@@ -3971,32 +2661,39 @@ async function handleDiff(args: string[]): Promise<void> {
3971
2661
  console.log(`\nProfiling ${dbType} database...\n`);
3972
2662
  let profiles: TableProfile[];
3973
2663
  try {
2664
+ let result: ProfilingResult;
3974
2665
  switch (dbType) {
3975
2666
  case "mysql":
3976
- profiles = await profileMySQL(connStr, filterTables);
2667
+ result = await profileMySQL(connStr, filterTables, undefined, undefined, cliProfileLogger);
3977
2668
  break;
3978
2669
  case "postgres":
3979
- profiles = await profilePostgres(connStr, filterTables, undefined, schemaArg);
2670
+ result = await profilePostgres(connStr, filterTables, undefined, schemaArg, undefined, cliProfileLogger);
3980
2671
  break;
3981
2672
  case "clickhouse":
3982
- profiles = await profileClickHouse(connStr, filterTables);
2673
+ result = await profileClickHouse(connStr, filterTables);
3983
2674
  break;
3984
2675
  case "snowflake":
3985
- profiles = await profileSnowflake(connStr, filterTables);
2676
+ result = await profileSnowflake(connStr, filterTables);
3986
2677
  break;
3987
2678
  case "duckdb": {
3988
2679
  const { parseDuckDBUrl } = await import("../../../plugins/duckdb/src/connection");
3989
2680
  const duckConfig = parseDuckDBUrl(connStr);
3990
- profiles = await profileDuckDB(duckConfig.path, filterTables);
2681
+ result = await profileDuckDB(duckConfig.path, filterTables);
3991
2682
  break;
3992
2683
  }
3993
2684
  case "salesforce":
3994
- profiles = await profileSalesforce(connStr, filterTables);
2685
+ result = await profileSalesforce(connStr, filterTables);
3995
2686
  break;
3996
2687
  default: {
3997
2688
  throw new Error(`Unknown database type: ${dbType}`);
3998
2689
  }
3999
2690
  }
2691
+ profiles = result.profiles;
2692
+ if (result.errors.length > 0) {
2693
+ const total = result.profiles.length + result.errors.length;
2694
+ logProfilingErrors(result.errors, total);
2695
+ console.warn(`Continuing diff with ${profiles.length} successfully profiled tables.\n`);
2696
+ }
4000
2697
  } catch (err) {
4001
2698
  console.error(`\nError: Failed to profile database.`);
4002
2699
  console.error(err instanceof Error ? err.message : String(err));
@@ -4009,7 +2706,7 @@ async function handleDiff(args: string[]): Promise<void> {
4009
2706
  }
4010
2707
 
4011
2708
  // Run FK inference so inferred FKs are comparable
4012
- analyzeTableProfiles(profiles);
2709
+ profiles = analyzeTableProfiles(profiles);
4013
2710
 
4014
2711
  // Build DB snapshots
4015
2712
  const dbSnapshots = new Map<string, EntitySnapshot>();
@@ -4058,7 +2755,7 @@ async function handleDiff(args: string[]): Promise<void> {
4058
2755
 
4059
2756
  // --- Profile a single datasource ---
4060
2757
 
4061
- export interface ProfileDatasourceOpts {
2758
+ interface ProfileDatasourceOpts {
4062
2759
  id: string; // "default", "warehouse", etc.
4063
2760
  url: string;
4064
2761
  dbType: DBType;
@@ -4067,25 +2764,18 @@ export interface ProfileDatasourceOpts {
4067
2764
  shouldEnrich: boolean;
4068
2765
  explicitEnrich: boolean;
4069
2766
  demoDataset: DemoDataset | null; // null for multi-source runs (--demo is single-datasource only)
2767
+ force: boolean; // skip failure threshold check
2768
+ orgId?: string; // org-scoped mode: write to semantic/.orgs/{orgId}/
4070
2769
  }
4071
2770
 
4072
- /**
4073
- * Compute the output base directory for a datasource.
4074
- * "default" → `semantic/`, anything else → `semantic/{id}/`.
4075
- * Returns an absolute path resolved from the process working directory.
4076
- */
4077
- export function outputDirForDatasource(id: string): string {
4078
- return id === "default" ? SEMANTIC_DIR : path.join(SEMANTIC_DIR, id);
4079
- }
4080
-
4081
- export interface DatasourceEntry {
2771
+ interface DatasourceEntry {
4082
2772
  id: string;
4083
2773
  url: string;
4084
2774
  schema: string;
4085
2775
  }
4086
2776
 
4087
2777
  async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4088
- const { id, url: connStr, dbType, filterTables, shouldEnrich, explicitEnrich, demoDataset } = opts;
2778
+ const { id, url: connStr, dbType, filterTables, shouldEnrich, explicitEnrich, demoDataset, force, orgId } = opts;
4089
2779
  let { schema: schemaArg } = opts;
4090
2780
 
4091
2781
  validateSchemaName(schemaArg);
@@ -4176,14 +2866,11 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4176
2866
  const DuckDBInstance = await loadDuckDB();
4177
2867
  const testInstance = await DuckDBInstance.create(duckConfig.path, { access_mode: "READ_ONLY" });
4178
2868
  const testConn = await testInstance.connect();
4179
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4180
- const reader = await (testConn as any).runAndReadAll("SELECT version() as v");
2869
+ const reader = await testConn.runAndReadAll("SELECT version() as v");
4181
2870
  const version = reader.getRowObjects()[0]?.v ?? "unknown";
4182
2871
  console.log(`Connected: DuckDB ${version}`);
4183
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4184
- (testConn as any).disconnectSync();
4185
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4186
- (testInstance as any).closeSync();
2872
+ testConn.disconnectSync();
2873
+ testInstance.closeSync();
4187
2874
  } catch (err) {
4188
2875
  console.error(`\nError: Cannot open DuckDB database.`);
4189
2876
  console.error(err instanceof Error ? err.message : String(err));
@@ -4193,8 +2880,7 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4193
2880
  } else if (dbType === "salesforce") {
4194
2881
  const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
4195
2882
  const config = parseSalesforceURL(connStr);
4196
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4197
- const source: any = createSalesforceConnection(config);
2883
+ const source = createSalesforceConnection(config);
4198
2884
  try {
4199
2885
  const objects = await source.listObjects();
4200
2886
  console.log(`Connected: Salesforce (${objects.length} queryable objects)`);
@@ -4232,10 +2918,10 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4232
2918
  try {
4233
2919
  switch (dbType) {
4234
2920
  case "mysql":
4235
- allObjects = await listMySQLObjects(connStr);
2921
+ allObjects = await listMySQLObjects(connStr, cliProfileLogger);
4236
2922
  break;
4237
2923
  case "postgres":
4238
- allObjects = await listPostgresObjects(connStr, schemaArg);
2924
+ allObjects = await listPostgresObjects(connStr, schemaArg, cliProfileLogger);
4239
2925
  break;
4240
2926
  case "clickhouse":
4241
2927
  allObjects = await listClickHouseObjects(connStr);
@@ -4298,40 +2984,66 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4298
2984
 
4299
2985
  console.log(`\nAtlas Init — profiling ${dbType} database...\n`);
4300
2986
 
4301
- let profiles: TableProfile[];
2987
+ const progress = createProgressTracker();
2988
+ const profilingStart = Date.now();
2989
+
2990
+ let result: ProfilingResult;
4302
2991
  switch (dbType) {
4303
2992
  case "mysql":
4304
- profiles = await profileMySQL(connStr, selectedTables, prefetchedObjects);
2993
+ result = await profileMySQL(connStr, selectedTables, prefetchedObjects, progress, cliProfileLogger);
4305
2994
  break;
4306
2995
  case "postgres":
4307
- profiles = await profilePostgres(connStr, selectedTables, prefetchedObjects, schemaArg);
2996
+ result = await profilePostgres(connStr, selectedTables, prefetchedObjects, schemaArg, progress, cliProfileLogger);
4308
2997
  break;
4309
2998
  case "clickhouse":
4310
- profiles = await profileClickHouse(connStr, selectedTables, prefetchedObjects);
2999
+ result = await profileClickHouse(connStr, selectedTables, prefetchedObjects, progress);
4311
3000
  break;
4312
3001
  case "snowflake":
4313
- profiles = await profileSnowflake(connStr, selectedTables, prefetchedObjects);
3002
+ result = await profileSnowflake(connStr, selectedTables, prefetchedObjects, progress);
4314
3003
  break;
4315
3004
  case "duckdb": {
4316
3005
  const { parseDuckDBUrl } = await import("../../../plugins/duckdb/src/connection");
4317
3006
  const duckConfig = parseDuckDBUrl(connStr);
4318
- profiles = await profileDuckDB(duckConfig.path, selectedTables, prefetchedObjects);
3007
+ result = await profileDuckDB(duckConfig.path, selectedTables, prefetchedObjects, progress);
4319
3008
  break;
4320
3009
  }
4321
3010
  case "salesforce":
4322
- profiles = await profileSalesforce(connStr, selectedTables, prefetchedObjects);
3011
+ result = await profileSalesforce(connStr, selectedTables, prefetchedObjects, progress);
4323
3012
  break;
4324
3013
  default: {
4325
3014
  throw new Error(`Unknown database type: ${dbType}`);
4326
3015
  }
4327
3016
  }
4328
3017
 
3018
+ let { profiles } = result;
3019
+ const { errors: profilingErrors } = result;
3020
+ const profilingElapsed = Date.now() - profilingStart;
3021
+ progress.onComplete(profiles.length, profilingElapsed);
3022
+
4329
3023
  if (profiles.length === 0) {
4330
3024
  throw new Error("No tables or views were successfully profiled. Check the warnings above and verify your database permissions.");
4331
3025
  }
4332
3026
 
3027
+ // Always warn about profiling errors
3028
+ if (profilingErrors.length > 0) {
3029
+ const totalAttempted = profiles.length + profilingErrors.length;
3030
+ logProfilingErrors(profilingErrors, totalAttempted);
3031
+
3032
+ const { shouldAbort } = checkFailureThreshold(result, force);
3033
+ if (shouldAbort) {
3034
+ console.error(`\nThis usually indicates a connection or permission issue.`);
3035
+ console.error(`Run \`atlas doctor\` to diagnose. Use \`--force\` to continue anyway.`);
3036
+ throw new Error(
3037
+ `Profiling failed for ${profilingErrors.length}/${totalAttempted} tables ` +
3038
+ `(${Math.round((profilingErrors.length / totalAttempted) * 100)}%). ` +
3039
+ `Use --force to continue anyway.`
3040
+ );
3041
+ }
3042
+ console.warn(`Continuing with ${profiles.length} successfully profiled tables.\n`);
3043
+ }
3044
+
4333
3045
  // Run profiler heuristics
4334
- analyzeTableProfiles(profiles);
3046
+ profiles = analyzeTableProfiles(profiles);
4335
3047
 
4336
3048
  const tableCount = profiles.filter((p) => !isViewLike(p)).length;
4337
3049
  const viewCount = profiles.filter((p) => isView(p)).length;
@@ -4371,7 +3083,7 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4371
3083
  }
4372
3084
 
4373
3085
  // Compute output directories
4374
- const outputBase = outputDirForDatasource(id);
3086
+ const outputBase = outputDirForDatasource(id, orgId);
4375
3087
  const entitiesOutDir = path.join(outputBase, "entities");
4376
3088
  const metricsOutDir = path.join(outputBase, "metrics");
4377
3089
 
@@ -4379,6 +3091,15 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4379
3091
  fs.mkdirSync(entitiesOutDir, { recursive: true });
4380
3092
  fs.mkdirSync(metricsOutDir, { recursive: true });
4381
3093
 
3094
+ // Clean stale entity/metric files from previous runs
3095
+ for (const dir of [entitiesOutDir, metricsOutDir]) {
3096
+ for (const file of fs.readdirSync(dir)) {
3097
+ if (file.endsWith(".yml") || file.endsWith(".yaml")) {
3098
+ fs.unlinkSync(path.join(dir, file));
3099
+ }
3100
+ }
3101
+ }
3102
+
4382
3103
  // Generate entity YAMLs
4383
3104
  console.log(`\nGenerating semantic layer...\n`);
4384
3105
 
@@ -4438,9 +3159,11 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
4438
3159
  }
4439
3160
  }
4440
3161
 
4441
- const relativeOutput = id === "default" ? "./semantic/" : `./semantic/${id}/`;
3162
+ const relativeOutput = orgId
3163
+ ? `./semantic/.orgs/${orgId}/`
3164
+ : id === "default" ? "./semantic/" : `./semantic/${id}/`;
4442
3165
  console.log(`
4443
- Done! Semantic layer for "${id}" is at ${relativeOutput}
3166
+ Done! Semantic layer written to ${relativeOutput} in ${formatDuration(profilingElapsed)}
4444
3167
 
4445
3168
  Generated:
4446
3169
  - ${profiles.length} entity YAMLs with dimensions, joins, measures, and query patterns${sourceId ? ` (connection: ${id})` : ""}
@@ -4454,6 +3177,82 @@ Next steps:
4454
3177
  `);
4455
3178
  }
4456
3179
 
3180
+ // --- Import ---
3181
+
3182
+ async function handleImport(args: string[]): Promise<void> {
3183
+ const connectionArg = getFlag(args, "--connection");
3184
+
3185
+ // Determine the API base URL
3186
+ const apiUrl = process.env.ATLAS_API_URL ?? "http://localhost:3001";
3187
+
3188
+ // Build the import request
3189
+ const importUrl = `${apiUrl}/api/v1/admin/semantic/org/import`;
3190
+ const body: Record<string, string> = {};
3191
+ if (connectionArg) body.connectionId = connectionArg;
3192
+
3193
+ // Determine auth header
3194
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
3195
+ if (process.env.ATLAS_API_KEY) headers.Authorization = `Bearer ${process.env.ATLAS_API_KEY}`;
3196
+
3197
+ console.log("Importing semantic layer from disk to DB...\n");
3198
+
3199
+ try {
3200
+ const resp = await fetch(importUrl, {
3201
+ method: "POST",
3202
+ headers,
3203
+ body: JSON.stringify(body),
3204
+ signal: AbortSignal.timeout(60_000),
3205
+ });
3206
+
3207
+ if (!resp.ok) {
3208
+ if (resp.status === 401 || resp.status === 403) {
3209
+ console.error("Import failed: authentication required.");
3210
+ console.error(" Set ATLAS_API_KEY environment variable.");
3211
+ } else {
3212
+ let errorMsg = `HTTP ${resp.status}`;
3213
+ try {
3214
+ const json = await resp.json() as { message?: string; error?: string };
3215
+ errorMsg = json.message ?? json.error ?? errorMsg;
3216
+ } catch {
3217
+ // intentionally ignored: JSON parse failed, fall through to text() attempt
3218
+ errorMsg = await resp.text().catch(() => errorMsg);
3219
+ }
3220
+ console.error(`Import failed: ${errorMsg}`);
3221
+ }
3222
+ process.exit(1);
3223
+ }
3224
+
3225
+ const result = await resp.json() as { imported: number; skipped: number; errors: Array<{ file: string; reason: string }>; total: number };
3226
+
3227
+ console.log(`Imported: ${result.imported}`);
3228
+ if (result.skipped > 0) {
3229
+ console.log(`Skipped: ${result.skipped}`);
3230
+ }
3231
+ console.log(`Total: ${result.total}`);
3232
+
3233
+ if (result.errors.length > 0) {
3234
+ console.log("\nErrors:");
3235
+ for (const e of result.errors) {
3236
+ console.log(` ${e.file}: ${e.reason}`);
3237
+ }
3238
+ }
3239
+
3240
+ if (result.imported > 0) {
3241
+ console.log("\nDone! Entities imported to DB. The explore tool and SQL validation will use the updated semantic layer.");
3242
+ }
3243
+ } catch (err) {
3244
+ const detail = err instanceof Error ? err.message : String(err);
3245
+ if (detail.includes("ECONNREFUSED") || detail.includes("fetch failed")) {
3246
+ console.error(`Cannot reach Atlas API at ${apiUrl}. Is the server running?`);
3247
+ console.error(" Start it with: bun run dev:api");
3248
+ console.error(" Set ATLAS_API_URL if the API is not on localhost:3001");
3249
+ } else {
3250
+ console.error(`Import failed: ${detail}`);
3251
+ }
3252
+ process.exit(1);
3253
+ }
3254
+ }
3255
+
4457
3256
  // --- Migrate ---
4458
3257
 
4459
3258
  async function handleMigrate(args: string[]): Promise<void> {
@@ -4569,12 +3368,306 @@ async function handleMigrate(args: string[]): Promise<void> {
4569
3368
  }
4570
3369
  }
4571
3370
 
3371
+ // --- Help system ---
3372
+
3373
+ interface SubcommandHelp {
3374
+ description: string;
3375
+ usage: string;
3376
+ flags?: Array<{ flag: string; description: string }>;
3377
+ subcommands?: Array<{ name: string; description: string }>;
3378
+ examples?: string[];
3379
+ }
3380
+
3381
+ function printSubcommandHelp(help: SubcommandHelp): void {
3382
+ console.log(`${help.description}\n`);
3383
+ console.log(`Usage: atlas ${help.usage}\n`);
3384
+ if (help.subcommands?.length) {
3385
+ console.log("Subcommands:");
3386
+ const maxLen = Math.max(...help.subcommands.map((s) => s.name.length));
3387
+ for (const s of help.subcommands) {
3388
+ console.log(` ${s.name.padEnd(maxLen + 2)}${s.description}`);
3389
+ }
3390
+ console.log();
3391
+ }
3392
+ if (help.flags?.length) {
3393
+ console.log("Options:");
3394
+ const maxLen = Math.max(...help.flags.map((f) => f.flag.length));
3395
+ for (const f of help.flags) {
3396
+ console.log(` ${f.flag.padEnd(maxLen + 2)}${f.description}`);
3397
+ }
3398
+ console.log();
3399
+ }
3400
+ if (help.examples?.length) {
3401
+ console.log("Examples:");
3402
+ for (const ex of help.examples) {
3403
+ console.log(` ${ex}`);
3404
+ }
3405
+ console.log();
3406
+ }
3407
+ }
3408
+
3409
+ const SUBCOMMAND_HELP: Record<string, SubcommandHelp> = {
3410
+ init: {
3411
+ description: "Profile a database and generate semantic layer YAML files.",
3412
+ usage: "init [options]",
3413
+ flags: [
3414
+ { flag: "--tables <t1,t2>", description: "Profile only specific tables/views (comma-separated)" },
3415
+ { flag: "--schema <name>", description: "PostgreSQL schema name (default: public)" },
3416
+ { flag: "--source <name>", description: "Write to semantic/{name}/ subdirectory (mutually exclusive with --connection)" },
3417
+ { flag: "--connection <name>", description: "Profile a named datasource from atlas.config.ts (mutually exclusive with --source)" },
3418
+ { flag: "--csv <file1.csv,...>", description: "Load CSV files via DuckDB (no DB server needed, requires @duckdb/node-api)" },
3419
+ { flag: "--parquet <f1.parquet,...>", description: "Load Parquet files via DuckDB (requires @duckdb/node-api)" },
3420
+ { flag: "--enrich", description: "Add LLM-enriched descriptions and query patterns (requires API key)" },
3421
+ { flag: "--no-enrich", description: "Explicitly skip LLM enrichment" },
3422
+ { flag: "--force", description: "Continue even if more than 20% of tables fail to profile" },
3423
+ { flag: "--demo [simple|cybersec|ecommerce]", description: "Load a demo dataset then profile (default: simple)" },
3424
+ { flag: "--org <orgId>", description: "Write to semantic/.orgs/{orgId}/ and auto-import to DB (org-scoped mode)" },
3425
+ { flag: "--no-import", description: "Skip auto-import to DB in org-scoped mode (write disk only)" },
3426
+ ],
3427
+ examples: [
3428
+ "atlas init",
3429
+ "atlas init --tables users,orders,products",
3430
+ "atlas init --enrich",
3431
+ "atlas init --demo cybersec",
3432
+ "atlas init --csv sales.csv,products.csv",
3433
+ "atlas init --org org-123",
3434
+ ],
3435
+ },
3436
+ diff: {
3437
+ description: "Compare the database schema against the existing semantic layer. Exits with code 1 if drift is detected.",
3438
+ usage: "diff [options]",
3439
+ flags: [
3440
+ { flag: "--tables <t1,t2>", description: "Diff only specific tables/views" },
3441
+ { flag: "--schema <name>", description: "PostgreSQL schema (falls back to ATLAS_SCHEMA, then public)" },
3442
+ { flag: "--source <name>", description: "Read from semantic/{name}/ subdirectory" },
3443
+ ],
3444
+ examples: [
3445
+ "atlas diff",
3446
+ "atlas diff --tables users,orders",
3447
+ 'atlas diff || echo "Schema drift detected!"',
3448
+ ],
3449
+ },
3450
+ query: {
3451
+ description: "Ask a natural language question and get an answer. Requires a running Atlas API server.",
3452
+ usage: 'query "your question" [options]',
3453
+ flags: [
3454
+ { flag: "--json", description: "Raw JSON output (pipe-friendly)" },
3455
+ { flag: "--csv", description: "CSV output (headers + rows, no narrative)" },
3456
+ { flag: "--quiet", description: "Data only — no narrative, SQL, or stats" },
3457
+ { flag: "--auto-approve", description: "Auto-approve any pending actions" },
3458
+ { flag: "--connection <id>", description: "Query a specific datasource" },
3459
+ ],
3460
+ examples: [
3461
+ 'atlas query "How many users signed up last month?"',
3462
+ 'atlas query "top 10 customers by revenue" --json',
3463
+ 'atlas query "monthly revenue by product" --csv > report.csv',
3464
+ ],
3465
+ },
3466
+ doctor: {
3467
+ description: "Alias for 'atlas validate' — validate config, semantic layer, and connectivity.",
3468
+ usage: "doctor",
3469
+ examples: [
3470
+ "atlas doctor",
3471
+ ],
3472
+ },
3473
+ validate: {
3474
+ description: "Validate config, semantic layer, and connectivity. Use --offline to skip connectivity checks.",
3475
+ usage: "validate [options]",
3476
+ flags: [
3477
+ { flag: "--offline", description: "Skip connectivity checks (datasource, provider, internal DB)" },
3478
+ ],
3479
+ examples: [
3480
+ "atlas validate",
3481
+ "atlas validate --offline",
3482
+ ],
3483
+ },
3484
+ mcp: {
3485
+ description: "Start an MCP (Model Context Protocol) server for Claude Desktop, Cursor, and other MCP clients.",
3486
+ usage: "mcp [options]",
3487
+ flags: [
3488
+ { flag: "--transport <stdio|sse>", description: "Transport type (default: stdio)" },
3489
+ { flag: "--port <n>", description: "Port for SSE transport (default: 8080)" },
3490
+ ],
3491
+ examples: [
3492
+ "atlas mcp",
3493
+ "atlas mcp --transport sse --port 9090",
3494
+ ],
3495
+ },
3496
+ import: {
3497
+ description: "Import semantic layer YAML files from disk into the internal DB for the active org.",
3498
+ usage: "import [options]",
3499
+ flags: [
3500
+ { flag: "--connection <name>", description: "Associate imported entities with a named datasource" },
3501
+ ],
3502
+ examples: [
3503
+ "atlas import",
3504
+ "atlas import --connection warehouse",
3505
+ ],
3506
+ },
3507
+ index: {
3508
+ description: "Rebuild the semantic index from current YAML files, or print index statistics.",
3509
+ usage: "index [options]",
3510
+ flags: [
3511
+ { flag: "--stats", description: "Print current index statistics without rebuilding" },
3512
+ ],
3513
+ examples: [
3514
+ "atlas index",
3515
+ "atlas index --stats",
3516
+ ],
3517
+ },
3518
+ learn: {
3519
+ description: "Analyze audit log and propose semantic layer YAML improvements.",
3520
+ usage: "learn [options]",
3521
+ flags: [
3522
+ { flag: "--apply", description: "Write proposed changes to YAML files (default: dry-run)" },
3523
+ { flag: "--suggestions", description: "Generate query suggestions from the audit log" },
3524
+ { flag: "--limit <n>", description: "Max audit log entries to analyze (default: 1000)" },
3525
+ { flag: "--since <date>", description: "Only analyze queries after this date (ISO 8601)" },
3526
+ { flag: "--source <name>", description: "Read from/write to semantic/{name}/ subdirectory" },
3527
+ ],
3528
+ examples: [
3529
+ "atlas learn",
3530
+ "atlas learn --apply",
3531
+ "atlas learn --since 2026-03-01 --limit 500",
3532
+ "atlas learn --source warehouse",
3533
+ ],
3534
+ },
3535
+ migrate: {
3536
+ description: "Generate or apply plugin schema migrations.",
3537
+ usage: "migrate [options]",
3538
+ flags: [
3539
+ { flag: "--apply", description: "Execute migrations against internal database (default: dry-run)" },
3540
+ ],
3541
+ examples: [
3542
+ "atlas migrate",
3543
+ "atlas migrate --apply",
3544
+ ],
3545
+ },
3546
+ plugin: {
3547
+ description: "Manage Atlas plugins.",
3548
+ usage: "plugin <list|create|add>",
3549
+ subcommands: [
3550
+ { name: "list", description: "List installed plugins from atlas.config.ts" },
3551
+ { name: "create <name> --type <type>", description: "Scaffold a new plugin (datasource|context|interaction|action|sandbox)" },
3552
+ { name: "add <package-name>", description: "Install a plugin package" },
3553
+ ],
3554
+ examples: [
3555
+ "atlas plugin list",
3556
+ "atlas plugin create my-plugin --type datasource",
3557
+ "atlas plugin add @useatlas/plugin-bigquery",
3558
+ ],
3559
+ },
3560
+ eval: {
3561
+ description: "Run the evaluation pipeline against demo schemas to measure text-to-SQL accuracy.",
3562
+ usage: "eval [options]",
3563
+ flags: [
3564
+ { flag: "--schema <name>", description: "Filter by demo dataset (not a PostgreSQL schema; e.g. simple, cybersec, ecommerce)" },
3565
+ { flag: "--category <name>", description: "Filter by category" },
3566
+ { flag: "--difficulty <level>", description: "Filter by difficulty (simple|medium|complex)" },
3567
+ { flag: "--id <case-id>", description: "Run a single case" },
3568
+ { flag: "--limit <n>", description: "Max cases to evaluate" },
3569
+ { flag: "--resume <file>", description: "Resume from existing JSONL results file" },
3570
+ { flag: "--baseline", description: "Save results as new baseline" },
3571
+ { flag: "--compare <file.jsonl>", description: "Diff against baseline (exit 1 on regression)" },
3572
+ { flag: "--csv", description: "CSV output" },
3573
+ { flag: "--json", description: "JSON summary output" },
3574
+ ],
3575
+ examples: [
3576
+ "atlas eval",
3577
+ "atlas eval --schema cybersec --difficulty complex",
3578
+ "atlas eval --baseline",
3579
+ ],
3580
+ },
3581
+ smoke: {
3582
+ description: "Run end-to-end smoke tests against a running Atlas deployment.",
3583
+ usage: "smoke [options]",
3584
+ flags: [
3585
+ { flag: "--target <url>", description: "API base URL (default: http://localhost:3001)" },
3586
+ { flag: "--api-key <key>", description: "Bearer auth token" },
3587
+ { flag: "--timeout <ms>", description: "Per-check timeout (default: 30000)" },
3588
+ { flag: "--verbose", description: "Show full response bodies on failure" },
3589
+ { flag: "--json", description: "Machine-readable JSON output" },
3590
+ ],
3591
+ examples: [
3592
+ "atlas smoke",
3593
+ "atlas smoke --target https://api.example.com --api-key sk-...",
3594
+ ],
3595
+ },
3596
+ benchmark: {
3597
+ description: "Run the BIRD benchmark for text-to-SQL accuracy evaluation.",
3598
+ usage: "benchmark [options]",
3599
+ flags: [
3600
+ { flag: "--bird-path <path>", description: "Path to the downloaded BIRD dev directory (required)" },
3601
+ { flag: "--limit <n>", description: "Max questions to evaluate" },
3602
+ { flag: "--db <name>", description: "Filter to a single database" },
3603
+ { flag: "--csv", description: "CSV output" },
3604
+ { flag: "--resume <file>", description: "Resume from existing JSONL results file" },
3605
+ ],
3606
+ examples: [
3607
+ "atlas benchmark --bird-path ./bird-dev",
3608
+ "atlas benchmark --bird-path ./bird-dev --db california_schools --limit 50",
3609
+ ],
3610
+ },
3611
+ completions: {
3612
+ description: "Output a shell completion script.",
3613
+ usage: "completions <bash|zsh|fish>",
3614
+ examples: [
3615
+ 'eval "$(atlas completions bash)"',
3616
+ 'eval "$(atlas completions zsh)"',
3617
+ "atlas completions fish > ~/.config/fish/completions/atlas.fish",
3618
+ ],
3619
+ },
3620
+ };
3621
+
3622
+ function printOverviewHelp(): void {
3623
+ console.log(
3624
+ "Atlas CLI — profile databases, generate semantic layers, and query your data.\n\n" +
3625
+ "Usage: atlas <command> [options]\n\n" +
3626
+ "Commands:\n" +
3627
+ " init Profile DB and generate semantic layer\n" +
3628
+ " import Import semantic YAML files from disk into DB\n" +
3629
+ " index Rebuild or inspect the semantic index\n" +
3630
+ " learn Analyze audit log and propose YAML improvements\n" +
3631
+ " diff Compare DB schema against existing semantic layer\n" +
3632
+ " query Ask a question via the Atlas API\n" +
3633
+ " validate Validate config, semantic layer, and connectivity\n" +
3634
+ " doctor Alias for validate\n" +
3635
+ " eval Run eval pipeline against demo schemas\n" +
3636
+ " smoke Run E2E smoke tests against a running Atlas deployment\n" +
3637
+ " migrate Generate/apply plugin schema migrations\n" +
3638
+ " plugin Manage plugins (list, create, add)\n" +
3639
+ " benchmark Run BIRD benchmark for text-to-SQL accuracy\n" +
3640
+ " mcp Start MCP server (stdio or SSE transport)\n" +
3641
+ " completions Output shell completion script (bash, zsh, fish)\n\n" +
3642
+ "Run atlas <command> --help for detailed usage of any command."
3643
+ );
3644
+ }
3645
+
3646
+ /** Check if args contain --help or -h for a subcommand. */
3647
+ function wantsHelp(args: string[]): boolean {
3648
+ return args.includes("--help") || args.includes("-h");
3649
+ }
3650
+
4572
3651
  // --- Main ---
4573
3652
 
4574
3653
  async function main() {
4575
3654
  const args = process.argv.slice(2);
4576
3655
  const command = args[0];
4577
3656
 
3657
+ // Top-level help: atlas --help, atlas -h, or no command
3658
+ if (!command || command === "--help" || command === "-h") {
3659
+ printOverviewHelp();
3660
+ process.exit(0);
3661
+ }
3662
+
3663
+ // Per-subcommand --help
3664
+ if (wantsHelp(args) && command in SUBCOMMAND_HELP) {
3665
+ printSubcommandHelp(SUBCOMMAND_HELP[command]);
3666
+ process.exit(0);
3667
+ }
3668
+
3669
+ await checkEnvFile(command);
3670
+
4578
3671
  if (command === "query") {
4579
3672
  return handleQuery(args);
4580
3673
  }
@@ -4601,17 +3694,28 @@ async function main() {
4601
3694
  }
4602
3695
 
4603
3696
  if (command === "doctor") {
4604
- const { runDoctor } = await import("../src/doctor");
4605
- const exitCode = await runDoctor();
3697
+ // doctor is an alias for validate with relaxed exit codes:
3698
+ // Sandbox and Internal DB failures don't contribute to exit 1
3699
+ const { runValidate } = await import("../src/validate");
3700
+ const exitCode = await runValidate({ mode: "doctor" });
4606
3701
  process.exit(exitCode);
4607
3702
  }
4608
3703
 
4609
3704
  if (command === "validate") {
4610
3705
  const { runValidate } = await import("../src/validate");
4611
- const exitCode = await runValidate();
3706
+ const offline = args.includes("--offline");
3707
+ const exitCode = await runValidate({ offline });
4612
3708
  process.exit(exitCode);
4613
3709
  }
4614
3710
 
3711
+ if (command === "index") {
3712
+ return handleIndex(args);
3713
+ }
3714
+
3715
+ if (command === "learn") {
3716
+ return handleLearn(args);
3717
+ }
3718
+
4615
3719
  if (command === "diff") {
4616
3720
  return handleDiff(args);
4617
3721
  }
@@ -4676,6 +3780,10 @@ async function main() {
4676
3780
  return;
4677
3781
  }
4678
3782
 
3783
+ if (command === "import") {
3784
+ return handleImport(args);
3785
+ }
3786
+
4679
3787
  if (command === "migrate") {
4680
3788
  return handleMigrate(args);
4681
3789
  }
@@ -4685,56 +3793,8 @@ async function main() {
4685
3793
  }
4686
3794
 
4687
3795
  if (command !== "init") {
4688
- console.log(
4689
- "Usage: bun run atlas -- <init|diff|query|doctor|validate|eval|smoke|migrate|plugin|benchmark|mcp|completions> [options]\n\n" +
4690
- "Commands:\n" +
4691
- " init Profile DB and generate semantic layer\n" +
4692
- " diff Compare DB schema against existing semantic layer\n" +
4693
- " query Ask a question via the Atlas API\n" +
4694
- " doctor Validate environment, connectivity, and configuration\n" +
4695
- " validate Check config and semantic layer YAML files (offline)\n" +
4696
- " eval Run eval pipeline against demo schemas\n" +
4697
- " smoke Run E2E smoke tests against a running Atlas deployment\n" +
4698
- " migrate Generate/apply plugin schema migrations\n" +
4699
- " plugin Manage plugins (list, create, add)\n" +
4700
- " benchmark Run BIRD benchmark for text-to-SQL accuracy\n" +
4701
- " mcp Start MCP server (stdio default, --transport sse --port N for SSE)\n" +
4702
- " completions Output shell completion script (bash, zsh, fish)\n\n" +
4703
- "Options (init):\n" +
4704
- " --tables t1,t2 Only specific tables/views\n" +
4705
- " --schema <name> PostgreSQL schema (default: public)\n" +
4706
- " --source <name> Write to semantic/{name}/ subdirectory (per-source layout)\n" +
4707
- " --connection <name> Profile a datasource from atlas.config.ts\n" +
4708
- " --csv file1.csv[,...] Load CSV files via DuckDB (no DB server needed)\n" +
4709
- " --parquet f1.parquet[,...] Load Parquet files via DuckDB\n" +
4710
- " --enrich Profile + LLM enrichment (needs API key)\n" +
4711
- " --no-enrich Explicitly skip LLM enrichment\n" +
4712
- " --demo [simple|cybersec|ecommerce] Load demo dataset then profile\n\n" +
4713
- "Options (diff):\n" +
4714
- " --tables t1,t2 Only diff specific tables/views\n" +
4715
- " --schema <name> PostgreSQL schema (default: public)\n" +
4716
- " --source <name> Read from semantic/{name}/ subdirectory\n\n" +
4717
- "Options (query):\n" +
4718
- ' atlas query "question" Ask a question (requires running API server)\n' +
4719
- " --json Raw JSON output (pipe-friendly)\n" +
4720
- " --csv CSV output (headers + rows, no narrative)\n" +
4721
- " --quiet Data only — no narrative, SQL, or stats\n" +
4722
- " --auto-approve Auto-approve any pending actions\n" +
4723
- " --connection <id> Query a specific datasource\n\n" +
4724
- "Options (migrate):\n" +
4725
- " atlas migrate Dry run — show SQL that would be executed\n" +
4726
- " --apply Execute migrations against internal DB\n\n" +
4727
- "Options (smoke):\n" +
4728
- " --target <url> API base URL (default: http://localhost:3001)\n" +
4729
- " --api-key <key> Bearer auth token\n" +
4730
- " --timeout <ms> Per-check timeout (default: 30000)\n" +
4731
- " --verbose Show full response bodies on failure\n" +
4732
- " --json Machine-readable JSON output\n\n" +
4733
- "Options (plugin):\n" +
4734
- " atlas plugin list List installed plugins\n" +
4735
- " atlas plugin create <name> --type <type> Scaffold a new plugin\n" +
4736
- " atlas plugin add <package-name> Install a plugin package"
4737
- );
3796
+ console.error(`Unknown command: ${command}\n`);
3797
+ printOverviewHelp();
4738
3798
  process.exit(1);
4739
3799
  }
4740
3800
 
@@ -4744,6 +3804,7 @@ async function main() {
4744
3804
  const sourceArg = requireFlagIdentifier(args, "--source", "source name");
4745
3805
  const connectionArg = requireFlagIdentifier(args, "--connection", "connection name");
4746
3806
  const demoDataset = parseDemoArg(args);
3807
+ const forceInit = args.includes("--force");
4747
3808
  const csvArg = getFlag(args, "--csv");
4748
3809
  const parquetArg = getFlag(args, "--parquet");
4749
3810
  const hasDocumentFiles = !!(csvArg || parquetArg);
@@ -4792,15 +3853,31 @@ async function main() {
4792
3853
  // Profile the DuckDB database
4793
3854
  console.log("Profiling DuckDB tables...\n");
4794
3855
  const duckFilterTables = filterTables ?? tableNames;
4795
- const profiles = await profileDuckDB(dbPath, duckFilterTables);
3856
+ const duckProgress = createProgressTracker();
3857
+ const duckStart = Date.now();
3858
+ const duckResult = await profileDuckDB(dbPath, duckFilterTables, undefined, duckProgress);
3859
+ let { profiles } = duckResult;
3860
+ duckProgress.onComplete(profiles.length, Date.now() - duckStart);
4796
3861
 
4797
3862
  if (profiles.length === 0) {
4798
3863
  console.error("\nError: No tables were successfully profiled.");
4799
3864
  process.exit(1);
4800
3865
  }
4801
3866
 
3867
+ // Warn about any profiling errors
3868
+ if (duckResult.errors.length > 0) {
3869
+ const total = profiles.length + duckResult.errors.length;
3870
+ logProfilingErrors(duckResult.errors, total);
3871
+ const { shouldAbort } = checkFailureThreshold(duckResult, forceInit);
3872
+ if (shouldAbort) {
3873
+ console.error(`\nUse \`--force\` to continue anyway.`);
3874
+ process.exit(1);
3875
+ }
3876
+ console.warn(`Continuing with ${profiles.length} successfully profiled tables.\n`);
3877
+ }
3878
+
4802
3879
  // Run profiler heuristics
4803
- analyzeTableProfiles(profiles);
3880
+ profiles = analyzeTableProfiles(profiles);
4804
3881
 
4805
3882
  console.log(`\nFound ${profiles.length} table(s):\n`);
4806
3883
  for (const p of profiles) {
@@ -4811,6 +3888,15 @@ async function main() {
4811
3888
  fs.mkdirSync(entitiesOutDir, { recursive: true });
4812
3889
  fs.mkdirSync(metricsOutDir, { recursive: true });
4813
3890
 
3891
+ // Clean stale entity/metric files from previous runs
3892
+ for (const dir of [entitiesOutDir, metricsOutDir]) {
3893
+ for (const file of fs.readdirSync(dir)) {
3894
+ if (file.endsWith(".yml") || file.endsWith(".yaml")) {
3895
+ fs.unlinkSync(path.join(dir, file));
3896
+ }
3897
+ }
3898
+ }
3899
+
4814
3900
  console.log(`\nGenerating semantic layer...\n`);
4815
3901
 
4816
3902
  // DuckDB uses PostgreSQL-compatible SQL — "public" schema is not meaningful
@@ -4889,6 +3975,20 @@ Next steps:
4889
3975
  shouldEnrich = providerConfigured;
4890
3976
  }
4891
3977
 
3978
+ // --- Detect org-scoped mode ---
3979
+ // When DATABASE_URL is set and managed auth is active, atlas init writes
3980
+ // to semantic/.orgs/{orgId}/ and auto-imports to the internal DB.
3981
+ const noImport = args.includes("--no-import");
3982
+ let orgId: string | undefined;
3983
+ if (process.env.DATABASE_URL && process.env.BETTER_AUTH_SECRET) {
3984
+ // Org-scoped mode is available. The orgId comes from the active session.
3985
+ // For CLI use, accept ATLAS_ORG_ID env var or --org flag.
3986
+ orgId = getFlag(args, "--org") ?? process.env.ATLAS_ORG_ID;
3987
+ if (orgId) {
3988
+ console.log(`Org-scoped mode: writing to semantic/.orgs/${orgId}/\n`);
3989
+ }
3990
+ }
3991
+
4892
3992
  // --- Resolve datasource list ---
4893
3993
 
4894
3994
  // Try loading atlas.config.ts
@@ -5048,6 +4148,8 @@ Next steps:
5048
4148
  shouldEnrich,
5049
4149
  explicitEnrich,
5050
4150
  demoDataset: isMultiSource ? null : demoDataset,
4151
+ force: forceInit,
4152
+ orgId,
5051
4153
  });
5052
4154
  } catch (err) {
5053
4155
  const msg = err instanceof Error ? err.message : String(err);
@@ -5071,6 +4173,66 @@ Next steps:
5071
4173
  console.error(`${"=".repeat(60)}`);
5072
4174
  process.exit(1);
5073
4175
  }
4176
+
4177
+ // --- Auto-import to DB in org-scoped mode ---
4178
+ if (orgId && !noImport) {
4179
+ console.log("\nImporting entities to internal DB...\n");
4180
+
4181
+ const apiUrl = process.env.ATLAS_API_URL ?? "http://localhost:3001";
4182
+ const importUrl = `${apiUrl}/api/v1/admin/semantic/org/import`;
4183
+ const importHeaders: Record<string, string> = { "Content-Type": "application/json" };
4184
+ if (process.env.ATLAS_API_KEY) importHeaders.Authorization = `Bearer ${process.env.ATLAS_API_KEY}`;
4185
+
4186
+ // For each datasource, import with its connection ID
4187
+ let anyImported = false;
4188
+ for (const ds of datasources) {
4189
+ const importBody: Record<string, string> = {};
4190
+ if (ds.id !== "default") importBody.connectionId = ds.id;
4191
+
4192
+ try {
4193
+ const resp = await fetch(importUrl, {
4194
+ method: "POST",
4195
+ headers: importHeaders,
4196
+ body: JSON.stringify(importBody),
4197
+ signal: AbortSignal.timeout(60_000),
4198
+ });
4199
+
4200
+ if (resp.ok) {
4201
+ const result = await resp.json() as { imported: number; skipped: number; total: number };
4202
+ console.log(` Imported ${result.imported} entities${ds.id !== "default" ? ` (connection: ${ds.id})` : ""}`);
4203
+ if (result.imported > 0) anyImported = true;
4204
+ } else {
4205
+ let errorMsg = `HTTP ${resp.status}`;
4206
+ try {
4207
+ const json = await resp.json() as { message?: string; error?: string };
4208
+ errorMsg = json.message ?? json.error ?? errorMsg;
4209
+ } catch {
4210
+ // intentionally ignored: JSON parse failed, fall through to text() attempt
4211
+ errorMsg = await resp.text().catch(() => errorMsg);
4212
+ }
4213
+ console.warn(` Warning: Import failed for ${ds.id}: ${errorMsg}`);
4214
+ console.warn(" Run 'atlas import' later to retry.\n");
4215
+ }
4216
+ } catch (err) {
4217
+ const detail = err instanceof Error ? err.message : String(err);
4218
+ if (detail.includes("ECONNREFUSED") || detail.includes("fetch failed")) {
4219
+ console.warn(" Warning: Atlas API not reachable — skipping auto-import.");
4220
+ console.warn(" Set ATLAS_API_URL if the API is not on localhost:3001");
4221
+ console.warn(" Start the API server and run 'atlas import' to import manually.\n");
4222
+ break; // Don't try remaining datasources
4223
+ }
4224
+ console.warn(` Warning: Import failed for ${ds.id}: ${detail}`);
4225
+ }
4226
+ }
4227
+
4228
+ if (!anyImported && datasources.length > 0) {
4229
+ console.warn("\nNo entities were imported to the DB. Files were written to disk successfully.");
4230
+ console.warn("Run 'atlas import' once the API server is available to complete the import.");
4231
+ if (!process.env.ATLAS_API_KEY) {
4232
+ console.warn("Hint: set ATLAS_API_KEY for CLI authentication.\n");
4233
+ }
4234
+ }
4235
+ }
5074
4236
  }
5075
4237
 
5076
4238
  export function getFlag(args: string[], flag: string): string | undefined {