databricks-tellr-app 0.3.11.dev5__tar.gz → 0.3.11.dev7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app.egg-info/SOURCES.txt +4 -0
  4. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/pyproject.toml +1 -1
  5. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/agent_config.py +42 -4
  6. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/settings/design_systems.py +330 -0
  7. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/agent_config.py +5 -0
  8. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/database.py +52 -6
  9. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/prompt_modules.py +53 -3
  10. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/__init__.py +4 -0
  11. databricks_tellr_app-0.3.11.dev7/src/database/models/design_system.py +357 -0
  12. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/agent_factory.py +63 -12
  13. databricks_tellr_app-0.3.11.dev7/src/services/design_system_compiler.py +603 -0
  14. databricks_tellr_app-0.3.11.dev7/src/services/design_system_service.py +1004 -0
  15. databricks_tellr_app-0.3.11.dev7/src/services/design_system_templates.py +641 -0
  16. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/tools/__init__.py +4 -0
  17. databricks_tellr_app-0.3.11.dev7/src/services/tools/ds_asset_tool.py +105 -0
  18. databricks_tellr_app-0.3.11.dev5/src/database/models/design_system.py +0 -198
  19. databricks_tellr_app-0.3.11.dev5/src/services/design_system_compiler.py +0 -277
  20. databricks_tellr_app-0.3.11.dev5/src/services/design_system_service.py +0 -482
  21. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/README.md +0 -0
  22. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app/__init__.py +0 -0
  23. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app/run.py +0 -0
  24. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  25. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app.egg-info/requires.txt +0 -0
  26. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  27. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/setup.cfg +0 -0
  28. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/setup.py +0 -0
  29. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/__init__.py +0 -0
  30. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/__init__.py +0 -0
  31. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/_otel_bootstrap.py +0 -0
  32. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/fixtures/tour_demo_deck.json +0 -0
  33. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/main.py +0 -0
  34. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/mcp_auth.py +0 -0
  35. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/mcp_server.py +0 -0
  36. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/middleware/__init__.py +0 -0
  37. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/middleware/request_logging.py +0 -0
  38. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/__init__.py +0 -0
  39. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/admin.py +0 -0
  40. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/chat.py +0 -0
  41. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/deck_contributors.py +0 -0
  42. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/export.py +0 -0
  43. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/feedback.py +0 -0
  44. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/google_slides.py +0 -0
  45. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/images.py +0 -0
  46. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/local_version.py +0 -0
  47. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/profiles.py +0 -0
  48. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/sessions.py +0 -0
  49. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/settings/__init__.py +0 -0
  50. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/settings/contributors.py +0 -0
  51. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/settings/deck_prompts.py +0 -0
  52. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/settings/identities.py +0 -0
  53. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/settings/slide_styles.py +0 -0
  54. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/setup.py +0 -0
  55. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/slides.py +0 -0
  56. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/tools.py +0 -0
  57. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/tour.py +0 -0
  58. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/verification.py +0 -0
  59. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/routes/version.py +0 -0
  60. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/__init__.py +0 -0
  61. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/feedback.py +0 -0
  62. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/requests.py +0 -0
  63. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/responses.py +0 -0
  64. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/settings/__init__.py +0 -0
  65. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/settings/requests.py +0 -0
  66. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/settings/responses.py +0 -0
  67. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/schemas/streaming.py +0 -0
  68. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/__init__.py +0 -0
  69. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/chat_service.py +0 -0
  70. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/export_job_queue.py +0 -0
  71. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/feedback_service.py +0 -0
  72. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/job_queue.py +0 -0
  73. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/session_manager.py +0 -0
  74. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/services/session_naming.py +0 -0
  75. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/utils/__init__.py +0 -0
  76. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/api/utils/validation.py +0 -0
  77. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/__init__.py +0 -0
  78. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/config_loader.py +0 -0
  79. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/context_utils.py +0 -0
  80. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/databricks_client.py +0 -0
  81. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/defaults.py +0 -0
  82. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/encryption.py +0 -0
  83. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/init_default_profile.py +0 -0
  84. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/lakebase.py +0 -0
  85. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/migrate_profiles_to_agent_config.py +0 -0
  86. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/mlflow_agent_spans.py +0 -0
  87. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/mlflow_tracing.py +0 -0
  88. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/otel_logging.py +0 -0
  89. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/permission_context.py +0 -0
  90. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/settings_db.py +0 -0
  91. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/core/user_context.py +0 -0
  92. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/__init__.py +0 -0
  93. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/deck_contributor.py +0 -0
  94. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/feedback.py +0 -0
  95. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/genie_space.py +0 -0
  96. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/google_global_credentials.py +0 -0
  97. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/google_oauth_token.py +0 -0
  98. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/identity.py +0 -0
  99. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/image.py +0 -0
  100. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/profile.py +0 -0
  101. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/profile_contributor.py +0 -0
  102. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/prompts.py +0 -0
  103. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/request_log.py +0 -0
  104. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/session.py +0 -0
  105. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/slide_deck_prompt.py +0 -0
  106. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/slide_style_library.py +0 -0
  107. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/database/models/user_preference.py +0 -0
  108. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/domain/__init__.py +0 -0
  109. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/domain/slide.py +0 -0
  110. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/domain/slide_deck.py +0 -0
  111. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/__init__.py +0 -0
  112. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/agent.py +0 -0
  113. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/config_service.py +0 -0
  114. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/config_validator.py +0 -0
  115. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/drive_uploader.py +0 -0
  116. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/evaluation/__init__.py +0 -0
  117. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/evaluation/llm_judge.py +0 -0
  118. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/genie_service.py +0 -0
  119. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/google_slides_auth.py +0 -0
  120. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/google_slides_prompts_defaults.py +0 -0
  121. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/html_to_google_slides.py +0 -0
  122. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/html_to_pptx.py +0 -0
  123. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/identity_provider.py +0 -0
  124. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/identity_providers/__init__.py +0 -0
  125. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/identity_providers/account_provider.py +0 -0
  126. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/identity_providers/local_provider.py +0 -0
  127. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/identity_providers/workspace_provider.py +0 -0
  128. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/image_service.py +0 -0
  129. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/image_tools.py +0 -0
  130. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/permission_service.py +0 -0
  131. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/pptx_from_html_huashu.py +0 -0
  132. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/pptx_from_records.py +0 -0
  133. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/pptx_prompts_defaults.py +0 -0
  134. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/profile_service.py +0 -0
  135. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/streaming_callback.py +0 -0
  136. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/tools/agent_bricks_tool.py +0 -0
  137. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/tools/genie_tool.py +0 -0
  138. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/tools/mcp_tool.py +0 -0
  139. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/tools/model_endpoint_tool.py +0 -0
  140. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/tools/vector_tool.py +0 -0
  141. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/services/validator.py +0 -0
  142. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/__init__.py +0 -0
  143. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/css_utils.py +0 -0
  144. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/ds_asset_utils.py +0 -0
  145. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/error_handling.py +0 -0
  146. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/html_safety.py +0 -0
  147. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/html_utils.py +0 -0
  148. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/image_utils.py +0 -0
  149. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/js_validator.py +0 -0
  150. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/logging_config.py +0 -0
  151. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/pi_filter.py +0 -0
  152. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/slide_hash.py +0 -0
  153. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/spotlight.py +0 -0
  154. {databricks_tellr_app-0.3.11.dev5 → databricks_tellr_app-0.3.11.dev7}/src/utils/text_caps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.3.11.dev5
3
+ Version: 0.3.11.dev7
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.3.11.dev5
3
+ Version: 0.3.11.dev7
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -99,6 +99,7 @@ setup.py
99
99
  ./src/services/config_validator.py
100
100
  ./src/services/design_system_compiler.py
101
101
  ./src/services/design_system_service.py
102
+ ./src/services/design_system_templates.py
102
103
  ./src/services/drive_uploader.py
103
104
  ./src/services/genie_service.py
104
105
  ./src/services/google_slides_auth.py
@@ -123,6 +124,7 @@ setup.py
123
124
  ./src/services/identity_providers/workspace_provider.py
124
125
  ./src/services/tools/__init__.py
125
126
  ./src/services/tools/agent_bricks_tool.py
127
+ ./src/services/tools/ds_asset_tool.py
126
128
  ./src/services/tools/genie_tool.py
127
129
  ./src/services/tools/mcp_tool.py
128
130
  ./src/services/tools/model_endpoint_tool.py
@@ -243,6 +245,7 @@ src/services/config_service.py
243
245
  src/services/config_validator.py
244
246
  src/services/design_system_compiler.py
245
247
  src/services/design_system_service.py
248
+ src/services/design_system_templates.py
246
249
  src/services/drive_uploader.py
247
250
  src/services/genie_service.py
248
251
  src/services/google_slides_auth.py
@@ -267,6 +270,7 @@ src/services/identity_providers/local_provider.py
267
270
  src/services/identity_providers/workspace_provider.py
268
271
  src/services/tools/__init__.py
269
272
  src/services/tools/agent_bricks_tool.py
273
+ src/services/tools/ds_asset_tool.py
270
274
  src/services/tools/genie_tool.py
271
275
  src/services/tools/mcp_tool.py
272
276
  src/services/tools/model_endpoint_tool.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr-app"
7
- version = "0.3.11.dev5"
7
+ version = "0.3.11.dev7"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -21,9 +21,25 @@ router = APIRouter(prefix="/api/sessions/{session_id}/agent-config", tags=["agen
21
21
 
22
22
 
23
23
  def _validate_references(config: AgentConfig) -> None:
24
- """Validate that design_system_id, slide_style_id and deck_prompt_id reference
25
- existing (active) library entries."""
26
- from src.database.models import DesignSystem, SlideDeckPromptLibrary, SlideStyleLibrary
24
+ """Validate the library references and SANITIZE the template pin, in place.
25
+
26
+ ``design_system_id``, ``slide_style_id`` and ``deck_prompt_id`` stay STRICT
27
+ (422 on a missing/inactive entry — the existing convention). The template
28
+ pin is SELF-HEALING instead: template ids are re-minted by the sanctioned
29
+ delete+re-upload workflow, so a persisted config can legitimately hold a
30
+ stale pin — rejecting it would wedge EVERY later config update. When
31
+ ``template_id`` does not resolve to a template belonging to the selected
32
+ design system (pin with no design system, another system's template, or a
33
+ missing/re-materialized template), it is cleared in place with a warning;
34
+ the caller persists and returns the sanitized config so the frontend state
35
+ syncs. This mirrors the deliberately lenient generation path.
36
+ """
37
+ from src.database.models import (
38
+ DesignSystem,
39
+ DesignSystemTemplate,
40
+ SlideDeckPromptLibrary,
41
+ SlideStyleLibrary,
42
+ )
27
43
 
28
44
  with get_db_session() as db:
29
45
  if config.design_system_id is not None:
@@ -40,6 +56,26 @@ def _validate_references(config: AgentConfig) -> None:
40
56
  status_code=422,
41
57
  detail=f"design_system_id {config.design_system_id} not found",
42
58
  )
59
+ if config.template_id is not None:
60
+ template = None
61
+ if config.design_system_id is not None:
62
+ template = (
63
+ db.query(DesignSystemTemplate)
64
+ .filter(
65
+ DesignSystemTemplate.id == config.template_id,
66
+ DesignSystemTemplate.design_system_id == config.design_system_id,
67
+ )
68
+ .first()
69
+ )
70
+ if template is None:
71
+ logger.warning(
72
+ "Clearing stale template pin %s (design_system_id=%s): the "
73
+ "template does not exist or does not belong to the selected "
74
+ "design system; saving the config without it",
75
+ config.template_id,
76
+ config.design_system_id,
77
+ )
78
+ config.template_id = None
43
79
  if config.slide_style_id is not None:
44
80
  style = (
45
81
  db.query(SlideStyleLibrary)
@@ -115,7 +151,9 @@ async def get_agent_config(session_id: str):
115
151
  async def put_agent_config(session_id: str, config: AgentConfig):
116
152
  """Replace the full agent config for a session."""
117
153
  # Pydantic already validated duplicates via model_validator.
118
- # Now validate foreign-key references.
154
+ # Now validate foreign-key references; a stale template pin is sanitized
155
+ # (cleared in place) rather than rejected, so what is persisted and
156
+ # returned below is the effective config.
119
157
  _validate_references(config)
120
158
 
121
159
  try:
@@ -15,6 +15,7 @@ silently overwritten; the caller supplies a different name (import accepts a
15
15
  """
16
16
  import logging
17
17
  import os
18
+ import re
18
19
  from typing import List, Optional
19
20
 
20
21
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Response, UploadFile, status
@@ -27,6 +28,8 @@ from src.database.models.design_system import (
27
28
  MAX_BUNDLE_SIZE_BYTES,
28
29
  DesignSystem,
29
30
  DesignSystemAsset,
31
+ DesignSystemFile,
32
+ DesignSystemTemplate,
30
33
  DesignSystemToken,
31
34
  )
32
35
  from src.services import design_system_service
@@ -65,6 +68,34 @@ class AssetOut(BaseModel):
65
68
  url: str # served-asset endpoint (bytes are never inlined in listings)
66
69
 
67
70
 
71
+ class TemplateOut(BaseModel):
72
+ """Picker view of one addressable template (Phase 4). The layout HTML is
73
+ deliberately NOT exposed here — source viewing is a later phase."""
74
+ id: int
75
+ name: str
76
+ description: Optional[str]
77
+ entry_path: str
78
+ thumbnail_url: Optional[str] # template-scoped thumbnail endpoint, or None
79
+
80
+
81
+ class DesignSystemTemplateListResponse(BaseModel):
82
+ templates: List[TemplateOut]
83
+ total: int
84
+
85
+
86
+ class FileEntryOut(BaseModel):
87
+ """One node of the source-file tree (Phase 6) — metadata only, never bytes."""
88
+ path: str
89
+ kind: str
90
+ mime: str
91
+ size_bytes: int
92
+
93
+
94
+ class DesignSystemFileListResponse(BaseModel):
95
+ files: List[FileEntryOut]
96
+ total: int
97
+
98
+
68
99
  class DesignSystemSummary(BaseModel):
69
100
  """List/picker view — counts only, no binary payloads."""
70
101
  id: int
@@ -473,12 +504,123 @@ def set_default_design_system(ds_id: int, db: Session = Depends(get_db)):
473
504
  )
474
505
 
475
506
 
507
+ def _template_thumbnail_url(ds_id: int, template_id: int) -> str:
508
+ return f"/api/settings/design-systems/{ds_id}/templates/{template_id}/thumbnail"
509
+
510
+
511
+ @router.get("/{ds_id}/templates", response_model=DesignSystemTemplateListResponse)
512
+ def list_design_system_templates(ds_id: int, db: Session = Depends(get_db)):
513
+ """List a design system's addressable templates for the picker.
514
+
515
+ Rows are materialized lazily (from the manifest + retained bundle files) for
516
+ systems imported before templates were addressable entities, then persisted;
517
+ a system without retained template files simply lists zero templates.
518
+ """
519
+ try:
520
+ ds = db.query(DesignSystem).filter(DesignSystem.id == ds_id).first()
521
+ if not ds:
522
+ raise HTTPException(
523
+ status_code=status.HTTP_404_NOT_FOUND,
524
+ detail=f"Design system {ds_id} not found",
525
+ )
526
+ from src.services.design_system_templates import materialize_templates
527
+
528
+ if materialize_templates(ds):
529
+ db.commit() # persist lazily-derived rows (and assign their ids)
530
+
531
+ templates = sorted(ds.templates, key=lambda t: t.id)
532
+ return DesignSystemTemplateListResponse(
533
+ templates=[
534
+ TemplateOut(
535
+ id=t.id,
536
+ name=t.name,
537
+ description=t.description,
538
+ entry_path=t.entry_path,
539
+ thumbnail_url=(
540
+ _template_thumbnail_url(ds_id, t.id)
541
+ if t.thumbnail_asset_id is not None
542
+ else None
543
+ ),
544
+ )
545
+ for t in templates
546
+ ],
547
+ total=len(templates),
548
+ )
549
+ except HTTPException:
550
+ raise
551
+ except Exception as e:
552
+ db.rollback()
553
+ logger.error(f"Error listing templates for design system {ds_id}: {e}", exc_info=True)
554
+ raise HTTPException(
555
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
556
+ detail="Failed to list design system templates",
557
+ )
558
+
559
+
476
560
  # Raster image types that are safe to render inline. Anything else (notably
477
561
  # image/svg+xml, which can carry inline <script>) is served as a download so a
478
562
  # directly-navigated asset cannot execute script in the app origin (stored XSS).
479
563
  _INLINE_SAFE_MIMES = frozenset({"image/png", "image/jpeg", "image/gif", "image/webp"})
480
564
 
481
565
 
566
+ @router.get("/{ds_id}/templates/{template_id}/thumbnail")
567
+ def serve_design_system_template_thumbnail(
568
+ ds_id: int, template_id: int, db: Session = Depends(get_db)
569
+ ):
570
+ """Serve a template's preview-screenshot bytes for the picker.
571
+
572
+ Ownership-validated end to end: the template must belong to the design
573
+ system in the path AND its thumbnail asset must belong to the same system —
574
+ 404 otherwise. Served with ``X-Content-Type-Options: nosniff`` (and forced
575
+ to download for non-raster types), mirroring the asset endpoint.
576
+ """
577
+ try:
578
+ template = (
579
+ db.query(DesignSystemTemplate)
580
+ .filter(
581
+ DesignSystemTemplate.id == template_id,
582
+ DesignSystemTemplate.design_system_id == ds_id,
583
+ )
584
+ .first()
585
+ )
586
+ if not template or template.thumbnail_asset_id is None:
587
+ raise HTTPException(
588
+ status_code=status.HTTP_404_NOT_FOUND,
589
+ detail=f"Thumbnail not found for template {template_id} "
590
+ f"of design system {ds_id}",
591
+ )
592
+ asset = (
593
+ db.query(DesignSystemAsset)
594
+ .filter(
595
+ DesignSystemAsset.id == template.thumbnail_asset_id,
596
+ DesignSystemAsset.design_system_id == ds_id,
597
+ )
598
+ .first()
599
+ )
600
+ if not asset:
601
+ raise HTTPException(
602
+ status_code=status.HTTP_404_NOT_FOUND,
603
+ detail=f"Thumbnail not found for template {template_id} "
604
+ f"of design system {ds_id}",
605
+ )
606
+ mime = str(asset.mime)
607
+ headers = {"X-Content-Type-Options": "nosniff"}
608
+ if mime not in _INLINE_SAFE_MIMES:
609
+ # Static value (no attacker-controlled filename) to avoid header injection.
610
+ headers["Content-Disposition"] = "attachment"
611
+ return Response(content=asset.data, media_type=mime, headers=headers)
612
+ except HTTPException:
613
+ raise
614
+ except Exception as e:
615
+ logger.error(
616
+ f"Error serving thumbnail for template {template_id}: {e}", exc_info=True
617
+ )
618
+ raise HTTPException(
619
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
620
+ detail="Failed to serve design system template thumbnail",
621
+ )
622
+
623
+
482
624
  @router.get("/{ds_id}/assets/{asset_id}")
483
625
  def serve_design_system_asset(ds_id: int, asset_id: int, db: Session = Depends(get_db)):
484
626
  """Serve a design-system asset's raw bytes (for preview + generation).
@@ -515,3 +657,191 @@ def serve_design_system_asset(ds_id: int, asset_id: int, db: Session = Depends(g
515
657
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
516
658
  detail="Failed to serve design system asset",
517
659
  )
660
+
661
+
662
+ # --- Source-file browser (v1 Phase 6) ----------------------------------------
663
+
664
+ # Extensions/MIMEs that are TEXT SOURCE material in the file browser. Anything
665
+ # matched here is served as ``text/plain`` so user-uploaded markup (HTML, SVG,
666
+ # JS, …) can never render or execute in the app origin — the browser is a SOURCE
667
+ # viewer, never a preview surface (template previews go through the thumbnail
668
+ # endpoint instead). The extension fallback covers rows whose stored MIME was
669
+ # unguessable at import time.
670
+ _TEXT_SOURCE_EXTENSIONS = frozenset(
671
+ ("md", "markdown", "css", "json", "html", "htm", "js", "mjs", "svg", "txt", "xml")
672
+ )
673
+ _TEXT_SOURCE_MIMES = frozenset(
674
+ (
675
+ "application/json",
676
+ "application/javascript",
677
+ "application/ecmascript",
678
+ "application/xml",
679
+ )
680
+ )
681
+
682
+ # Percent-encoded '.', '/' or '\' still present AFTER the framework's one decode
683
+ # pass — only ever seen in double-encoding smuggling attempts, so reject outright.
684
+ _ENCODED_TRAVERSAL_RE = re.compile(r"%(2e|2f|5c)", re.IGNORECASE)
685
+
686
+
687
+ def _validated_file_path(raw: str) -> Optional[str]:
688
+ """Return ``raw`` when it is a canonical bundle-relative path, else ``None``.
689
+
690
+ Stored ``design_system_file.path`` values are canonical by construction (the
691
+ importer normalizes them via ``_safe_relpath``), so the browser REJECTS any
692
+ non-canonical request instead of normalizing it into an accepted form:
693
+ backslashes, absolute/drive paths, empty/``.``/``..`` segments, and lingering
694
+ percent-encoded traversal bytes all fail. Lookups are DB-exact within one
695
+ design system (no filesystem involved), so this is defence-in-depth.
696
+ """
697
+ if not raw or "\\" in raw:
698
+ return None
699
+ # NUL / C0 control characters never appear in a legitimate stored path, and
700
+ # NUL in particular must be rejected BEFORE the DB lookup: psycopg2 refuses
701
+ # NUL in a bound parameter (ValueError), which would surface as a 500
702
+ # instead of the uniform opaque 404 (SQLite masks this as a no-match 404).
703
+ if any(ord(ch) < 0x20 for ch in raw):
704
+ return None
705
+ if _ENCODED_TRAVERSAL_RE.search(raw):
706
+ return None
707
+ if raw.startswith("/") or re.match(r"^[A-Za-z]:", raw):
708
+ return None
709
+ if any(segment in ("", ".", "..") for segment in raw.split("/")):
710
+ return None
711
+ return raw
712
+
713
+
714
+ def _is_text_source(path: str, mime: str) -> bool:
715
+ """True when a stored file is text source material (served as text/plain)."""
716
+ mime_l = (mime or "").lower()
717
+ if mime_l.startswith("text/"):
718
+ return True
719
+ if mime_l in _TEXT_SOURCE_MIMES or mime_l.endswith("+json") or mime_l.endswith("+xml"):
720
+ return True
721
+ base = path.rsplit("/", 1)[-1]
722
+ ext = base.rsplit(".", 1)[-1].lower() if "." in base else ""
723
+ return ext in _TEXT_SOURCE_EXTENSIONS
724
+
725
+
726
+ @router.get("/{ds_id}/files", response_model=DesignSystemFileListResponse)
727
+ def list_design_system_files(ds_id: int, db: Session = Depends(get_db)):
728
+ """List a design system's retained bundle file tree — metadata only.
729
+
730
+ ``design_system_file`` carries the COMPLETE tree for any system imported
731
+ since source retention (v1 Phase 1): SOURCE rows (readme/skill/css/template
732
+ HTML) plus path-only REFERENCE rows for every asset/font (their bytes live in
733
+ ``design_system_asset``). The listing is a column projection — byte payloads
734
+ are never loaded, mirroring the blob-free list conventions above.
735
+ """
736
+ try:
737
+ exists = db.query(DesignSystem.id).filter(DesignSystem.id == ds_id).first()
738
+ if not exists:
739
+ raise HTTPException(
740
+ status_code=status.HTTP_404_NOT_FOUND,
741
+ detail=f"Design system {ds_id} not found",
742
+ )
743
+ rows = (
744
+ db.query(
745
+ DesignSystemFile.path,
746
+ DesignSystemFile.kind,
747
+ DesignSystemFile.mime,
748
+ DesignSystemFile.size_bytes,
749
+ )
750
+ .filter(DesignSystemFile.design_system_id == ds_id)
751
+ .all()
752
+ )
753
+ # Sort in Python: deterministic byte order on every backend (SQL ORDER BY
754
+ # is collation-dependent on PostgreSQL).
755
+ rows = sorted(rows, key=lambda r: str(r[0]))
756
+ return DesignSystemFileListResponse(
757
+ files=[
758
+ FileEntryOut(path=path, kind=kind, mime=mime, size_bytes=size_bytes)
759
+ for path, kind, mime, size_bytes in rows
760
+ ],
761
+ total=len(rows),
762
+ )
763
+ except HTTPException:
764
+ raise
765
+ except Exception as e:
766
+ logger.error(f"Error listing files for design system {ds_id}: {e}", exc_info=True)
767
+ raise HTTPException(
768
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
769
+ detail="Failed to list design system files",
770
+ )
771
+
772
+
773
+ @router.get("/{ds_id}/files/{file_path:path}")
774
+ def serve_design_system_file(ds_id: int, file_path: str, db: Session = Depends(get_db)):
775
+ """Serve ONE stored bundle file's content — the "Open source file" endpoint.
776
+
777
+ Security posture for user-uploaded content (mirrors the thumbnail/asset
778
+ endpoints, hardened further because this endpoint serves markup sources):
779
+
780
+ - The requested path must be canonical (:func:`_validated_file_path` rejects
781
+ traversal, absolute, backslash and percent-encoded forms) and is looked up
782
+ by EXACT match scoped to this design system; reference rows resolve their
783
+ bytes through an ownership-checked ``design_system_asset`` lookup. Every
784
+ failure is the same opaque 404.
785
+ - Text sources (md/css/html/js/json/svg/…) are served as
786
+ ``text/plain; charset=utf-8`` — uploaded markup never gets a renderable or
787
+ executable content type.
788
+ - EVERY response is ``Content-Disposition: attachment`` (static value — no
789
+ attacker-controlled filename, no header injection) with
790
+ ``X-Content-Type-Options: nosniff``; nothing is served inline.
791
+ """
792
+ not_found = HTTPException(
793
+ status_code=status.HTTP_404_NOT_FOUND,
794
+ detail=f"File not found for design system {ds_id}",
795
+ )
796
+ try:
797
+ validated = _validated_file_path(file_path)
798
+ if validated is None:
799
+ raise not_found
800
+ row = (
801
+ db.query(DesignSystemFile)
802
+ .filter(
803
+ DesignSystemFile.design_system_id == ds_id,
804
+ DesignSystemFile.path == validated,
805
+ )
806
+ .first()
807
+ )
808
+ if not row:
809
+ raise not_found
810
+ content = row.data
811
+ if content is None:
812
+ # Asset/font reference row: bytes live in design_system_asset. The
813
+ # asset must belong to the SAME design system — 404 otherwise.
814
+ if row.asset_id is None:
815
+ raise not_found
816
+ asset = (
817
+ db.query(DesignSystemAsset)
818
+ .filter(
819
+ DesignSystemAsset.id == row.asset_id,
820
+ DesignSystemAsset.design_system_id == ds_id,
821
+ )
822
+ .first()
823
+ )
824
+ if not asset:
825
+ raise not_found
826
+ content = asset.data
827
+ media_type = (
828
+ "text/plain; charset=utf-8"
829
+ if _is_text_source(str(row.path), str(row.mime))
830
+ else str(row.mime)
831
+ )
832
+ return Response(
833
+ content=content,
834
+ media_type=media_type,
835
+ headers={
836
+ "X-Content-Type-Options": "nosniff",
837
+ "Content-Disposition": "attachment",
838
+ },
839
+ )
840
+ except HTTPException:
841
+ raise
842
+ except Exception as e:
843
+ logger.error(f"Error serving file for design system {ds_id}: {e}", exc_info=True)
844
+ raise HTTPException(
845
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
846
+ detail="Failed to serve design system file",
847
+ )
@@ -62,6 +62,11 @@ class AgentConfig(BaseModel):
62
62
  # A selected design system compiles to prompt text and, when set, takes
63
63
  # precedence over slide_style_id (see agent_factory._get_prompt_content).
64
64
  design_system_id: Optional[int] = None
65
+ # Optionally pins ONE of the selected design system's templates
66
+ # (design_system_template.id). Meaningful only alongside design_system_id;
67
+ # generation IGNORES it (with a log) unless the template exists and belongs
68
+ # to that design system, so a stale pin can never fail a request.
69
+ template_id: Optional[int] = None
65
70
  deck_prompt_id: Optional[int] = None
66
71
  system_prompt: Optional[str] = None
67
72
  slide_editing_instructions: Optional[str] = None
@@ -530,6 +530,9 @@ def _run_migrations(engine, schema: str | None = None):
530
530
  # --- design system: add is_active (soft-delete) to pre-existing tables ---
531
531
  _migrate_design_system_soft_delete(conn, inspector, schema, _qual, is_sqlite)
532
532
 
533
+ # --- design system (v1): add font_mapping_json to pre-existing tables ---
534
+ _migrate_design_system_font_mapping(conn, inspector, schema, _qual, is_sqlite)
535
+
533
536
  # --- keep newly created objects owned by the shared role (prod forks) ---
534
537
  _reassign_new_objects_to_shared_owner(conn, is_sqlite)
535
538
 
@@ -537,10 +540,14 @@ def _run_migrations(engine, schema: str | None = None):
537
540
  def _migrate_design_system_tables(conn, schema: str | None = None) -> None:
538
541
  """Create the additive design-system tables (idempotent, dialect-safe).
539
542
 
540
- Phase 1 of the Design System Library. Adds three NEW tables — ``design_system``,
541
- ``design_system_asset``, ``design_system_token`` — without touching
542
- ``slide_style_library`` or any existing table, so the current slide-style
543
- prompt path is unaffected.
543
+ Design System Library. Adds five NEW tables — ``design_system``,
544
+ ``design_system_asset``, ``design_system_token``, ``design_system_file``,
545
+ ``design_system_template`` — without touching ``slide_style_library`` or any
546
+ existing table, so the current slide-style prompt path is unaffected.
547
+ ``design_system_file`` (v1 Phase 1) retains bundle source files + path
548
+ references, and ``design_system_template`` (v1 Phase 4) makes templates
549
+ individually addressable; both are created AFTER their parents so their
550
+ foreign keys to ``design_system`` and ``design_system_asset`` resolve.
544
551
 
545
552
  ``Base.metadata.create_all()`` (run first in ``init_db``) already creates these
546
553
  on fresh installs; this hand-rolled step guarantees they also exist on
@@ -548,16 +555,24 @@ def _migrate_design_system_tables(conn, schema: str | None = None) -> None:
548
555
  exercised on its own. Creation is driven from the ORM metadata via
549
556
  ``Table.create(checkfirst=True)`` — a single source of truth for the schema,
550
557
  idempotent (a no-op when the table exists) and correctly compiled for both
551
- PostgreSQL/Lakebase and the SQLite used in tests. Parent table is created
558
+ PostgreSQL/Lakebase and the SQLite used in tests. Parent tables are created
552
559
  first so the child foreign keys resolve.
553
560
  """
554
561
  from src.database.models.design_system import (
555
562
  DesignSystem,
556
563
  DesignSystemAsset,
564
+ DesignSystemFile,
565
+ DesignSystemTemplate,
557
566
  DesignSystemToken,
558
567
  )
559
568
 
560
- for model in (DesignSystem, DesignSystemAsset, DesignSystemToken):
569
+ for model in (
570
+ DesignSystem,
571
+ DesignSystemAsset,
572
+ DesignSystemToken,
573
+ DesignSystemFile,
574
+ DesignSystemTemplate,
575
+ ):
561
576
  table = model.__table__
562
577
  # Match init_db()'s schema handling so a qualified deployment creates the
563
578
  # tables in the Lakebase schema; guarded so it stays a no-op on repeat.
@@ -599,6 +614,37 @@ def _migrate_design_system_soft_delete(conn, inspector, schema, _qual, is_sqlite
599
614
  ))
600
615
 
601
616
 
617
+ def _migrate_design_system_font_mapping(conn, inspector, schema, _qual, is_sqlite) -> None:
618
+ """Add ``font_mapping_json`` to ``design_system`` (v1 Phase 1, idempotent).
619
+
620
+ v1 Phase 1 stores a normalized font mapping (manifest ``fonts[]`` /
621
+ ``brandFonts[]`` -> family/weight/style + token linkage) on the parent record.
622
+ ``Base.metadata.create_all()`` adds the column on fresh installs, but does NOT
623
+ alter a ``design_system`` table provisioned by an earlier deploy; this
624
+ hand-rolled ALTER backfills it. The column is nullable (a bundle may declare
625
+ no fonts) so no default/backfill value is required. Column existence is probed
626
+ with the inspector passed from ``_run_migrations`` (or a fresh one when called
627
+ standalone). Dialect-safe: ``JSONB`` on PostgreSQL/Lakebase, ``JSON`` (TEXT
628
+ affinity) on the SQLite used in tests — matching the model's ``with_variant``
629
+ column and the existing ``agent_config JSON`` migration.
630
+ """
631
+ from sqlalchemy import inspect, text
632
+
633
+ insp = inspector or inspect(conn)
634
+ try:
635
+ cols = {c["name"] for c in insp.get_columns("design_system", schema=schema)}
636
+ except Exception:
637
+ return
638
+ if not cols or "font_mapping_json" in cols:
639
+ return
640
+
641
+ col_type = "JSON" if is_sqlite else "JSONB"
642
+ logger.info("Migration: adding font_mapping_json column to design_system")
643
+ conn.execute(text(
644
+ f"ALTER TABLE {_qual('design_system')} ADD COLUMN font_mapping_json {col_type} NULL"
645
+ ))
646
+
647
+
602
648
  def _reassign_new_objects_to_shared_owner(
603
649
  conn, is_sqlite: bool, owning_role: str = "tellr_app_owners"
604
650
  ) -> None: