tina4-python 3.13.61__tar.gz → 3.13.63__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 (168) hide show
  1. {tina4_python-3.13.61 → tina4_python-3.13.63}/.gitignore +4 -0
  2. {tina4_python-3.13.61 → tina4_python-3.13.63}/PKG-INFO +1 -1
  3. {tina4_python-3.13.61 → tina4_python-3.13.63}/pyproject.toml +1 -1
  4. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/__init__.py +1 -1
  5. tina4_python-3.13.63/tina4_python/context/__init__.py +371 -0
  6. tina4_python-3.13.63/tina4_python/context/chunker.py +120 -0
  7. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/crud/__init__.py +16 -8
  8. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/dev_admin/__init__.py +13 -0
  9. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/mcp/tools.py +36 -0
  10. {tina4_python-3.13.61 → tina4_python-3.13.63}/README.md +0 -0
  11. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/CLAUDE.md +0 -0
  12. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/HtmlElement.py +0 -0
  13. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/Testing.py +0 -0
  14. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/core/server.py +0 -0
  30. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/__init__.py +0 -0
  31. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/adapter.py +0 -0
  32. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/connection.py +0 -0
  33. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/firebird.py +0 -0
  34. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/mongodb.py +0 -0
  35. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/mssql.py +0 -0
  36. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/mysql.py +0 -0
  37. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/odbc.py +0 -0
  38. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/postgres.py +0 -0
  39. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/database/sqlite.py +0 -0
  40. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/debug/__init__.py +0 -0
  41. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/debug/error_overlay.py +0 -0
  42. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/dev_admin/plan.py +0 -0
  44. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/dev_admin/project_index.py +0 -0
  45. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/docs.py +0 -0
  46. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/docstore/__init__.py +0 -0
  47. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/dotenv/__init__.py +0 -0
  48. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/env.py +0 -0
  49. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/frond/FROND.md +0 -0
  50. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/frond/__init__.py +0 -0
  51. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/frond/engine.py +0 -0
  52. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/auth/meta.json +0 -0
  53. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  54. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/database/meta.json +0 -0
  55. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  56. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/error-overlay/meta.json +0 -0
  57. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  58. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/orm/meta.json +0 -0
  59. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  60. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  61. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/queue/meta.json +0 -0
  62. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  63. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/rest-api/meta.json +0 -0
  64. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  65. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/templates/meta.json +0 -0
  66. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  67. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  68. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/graphql/__init__.py +0 -0
  69. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/i18n/__init__.py +0 -0
  70. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/mcp/__init__.py +0 -0
  71. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/mcp/protocol.py +0 -0
  72. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/messenger/__init__.py +0 -0
  73. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/migration/__init__.py +0 -0
  74. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/migration/runner.py +0 -0
  75. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/orm/__init__.py +0 -0
  76. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/orm/fields.py +0 -0
  77. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/orm/model.py +0 -0
  78. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/__feedback/widget.js +0 -0
  79. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/css/tina4.css +0 -0
  80. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/css/tina4.min.css +0 -0
  81. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/favicon.ico +0 -0
  82. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/images/logo.svg +0 -0
  83. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  84. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/js/frond.js +0 -0
  85. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/js/frond.min.js +0 -0
  86. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  87. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  88. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/js/tina4.min.js +0 -0
  89. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/js/tina4js.min.js +0 -0
  90. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/swagger/index.html +0 -0
  91. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  92. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/query_builder/__init__.py +0 -0
  93. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue/__init__.py +0 -0
  94. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue/job.py +0 -0
  95. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue/kafka_backend.py +0 -0
  96. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue/lite_backend.py +0 -0
  97. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue/mongo_backend.py +0 -0
  98. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue_backends/__init__.py +0 -0
  100. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue_backends/kafka_backend.py +0 -0
  101. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue_backends/mongo_backend.py +0 -0
  102. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  103. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/__init__.py +0 -0
  104. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/models/Attachment.py +0 -0
  105. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/models/Channel.py +0 -0
  106. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/models/ChannelMember.py +0 -0
  107. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/models/Message.py +0 -0
  108. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/models/Workspace.py +0 -0
  109. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/models/__init__.py +0 -0
  110. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/realtime/storage.py +0 -0
  111. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/__init__.py +0 -0
  112. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  113. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_badges.scss +0 -0
  114. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  115. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_cards.scss +0 -0
  116. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_forms.scss +0 -0
  117. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_grid.scss +0 -0
  118. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_modals.scss +0 -0
  119. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_nav.scss +0 -0
  120. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_reset.scss +0 -0
  121. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_tables.scss +0 -0
  122. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_typography.scss +0 -0
  123. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  124. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/_variables.scss +0 -0
  125. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/base.scss +0 -0
  126. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/colors.scss +0 -0
  127. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/scss/tina4css/tina4.scss +0 -0
  128. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.61 → tina4_python-3.13.63}/tina4_python/wsdl/__init__.py +0 -0
@@ -46,6 +46,10 @@ test_queue*.db
46
46
  /publish.sh
47
47
  /src/scss/
48
48
  /broken
49
+ # Framework runtime artefacts under .tina4/ — the Context FTS5 index
50
+ # (.tina4/context.db), agent.log, and file-write backups. Transient,
51
+ # per-machine, never source-of-truth. Keep the whole dir out of git.
52
+ /.tina4/
49
53
  # Framework runtime artefacts — BrokenTracker writes import-time and
50
54
  # route-time failure dumps here. They're transient (timestamped per
51
55
  # error) and contain stack traces, never source-of-truth content.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.61
3
+ Version: 3.13.63
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.61"
3
+ version = "3.13.63"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.61"
11
+ __version__ = "3.13.63"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -0,0 +1,371 @@
1
+ # Tina4Python
2
+ #
3
+ # Context — a native, zero-dependency code/doc grounding index.
4
+ #
5
+ # Lets a Tina4 app ground its own AI assistant on its own source, offline:
6
+ # it walks the project, chunks code on def/class boundaries and docs as prose,
7
+ # and answers keyword/fuzzy queries over a SQLite FTS5 index (Python stdlib
8
+ # ``sqlite3`` — FTS5 + ``bm25()`` are built in, so NO new dependency).
9
+ #
10
+ # The retrieval core is a thin port of the proven slice of neemee
11
+ # (longmem-harness: memory_systems.SqliteFTS + pipeline.retrieve's stable
12
+ # source-over-tests / definition-first reorderings). It COMPLEMENTS the
13
+ # ``api_*`` reflection tools: ``api_*`` is exact structural lookup, Context is
14
+ # fuzzy/semantic FTS over source + docs.
15
+ #
16
+ # from tina4_python.context import Context
17
+ # ctx = Context(".tina4/context.db")
18
+ # ctx.index_root("src")
19
+ # ctx.search("where is the auth token issued?", k=5)
20
+ # # -> [{"path": "src/auth.py", "score": 2.31, "snippet": "..."}, ...]
21
+ #
22
+ # On-disk index defaults to ``.tina4/context.db`` (gitignored). Guards a
23
+ # sqlite build without FTS5: if absent, the Context degrades to safe no-ops
24
+ # rather than crashing the app.
25
+
26
+ import os
27
+ import re
28
+ import sqlite3
29
+ import threading
30
+ from pathlib import Path
31
+
32
+ from .chunker import (
33
+ chunk_code,
34
+ chunk_text,
35
+ fold,
36
+ light_stem,
37
+ terms,
38
+ )
39
+
40
+ __all__ = ["Context", "default_context", "existing_context"]
41
+
42
+ # File classification — mirrors neemee's repo walk.
43
+ CODE_EXTS = {".py", ".php", ".js", ".mjs", ".ts", ".rb",
44
+ ".pas", ".dpr", ".dpk", ".inc", ".dfm", ".fmx"}
45
+ DOC_EXTS = {".md", ".txt", ".rst", ".twig", ".html"}
46
+ # deploy/CLI/env answers live in config files, not sources — chunk as code
47
+ # (line windows), since sentence chunking shreds YAML/Dockerfiles.
48
+ CONFIG_EXTS = {".toml", ".yml", ".yaml"}
49
+ SPECIAL_FILES = {"dockerfile", "makefile", "docker-compose.yml",
50
+ "package.json", "composer.json",
51
+ ".env.example", ".env.sample"}
52
+ # Same dirs neemee skips, plus Tina4 runtime dirs that hold no source of truth
53
+ # (our own index/backups, session blobs, logs).
54
+ SKIP_DIRS = {".git", "__pycache__", "node_modules", "vendor", "dist",
55
+ "build", "coverage", ".idea", ".venv", "venv", ".pytest_cache",
56
+ ".tina4", "sessions", "logs"}
57
+
58
+ # generic question/code vocabulary that never NAMES a symbol — kept small.
59
+ _DEF_STOP = frozenset(
60
+ "the and what how does can which where when who why list all available "
61
+ "module class function functions method methods def get set new return "
62
+ "import from with that this are is was for into use used".split())
63
+
64
+ # a chunk that DEFINES a queried symbol ('def get_token', 'class Widget')
65
+ # should out-rank one that merely USES it. Matched against the folded body.
66
+ _DEF_KW = r"(?:async def|def|class|function|fn|func|interface|trait)"
67
+
68
+
69
+ def _fts5_supported() -> bool:
70
+ """True if this interpreter's sqlite3 was built with FTS5."""
71
+ try:
72
+ conn = sqlite3.connect(":memory:")
73
+ try:
74
+ conn.execute("CREATE VIRTUAL TABLE _probe USING fts5(x)")
75
+ return True
76
+ finally:
77
+ conn.close()
78
+ except sqlite3.OperationalError:
79
+ return False
80
+ except Exception:
81
+ return False
82
+
83
+
84
+ class Context:
85
+ """A SQLite FTS5 index over a project's source + docs.
86
+
87
+ Methods:
88
+ index_path(file, label=None) -> int upsert one file (delete-by-path,
89
+ re-chunk, insert); reindex-safe.
90
+ index_root(root) -> int walk a tree, index every eligible
91
+ file (skips vendor/build dirs).
92
+ search(query, k=5) -> list [{path, score, snippet}] ranked by
93
+ bm25() with source-over-tests +
94
+ definition-first reordering.
95
+ """
96
+
97
+ def __init__(self, path="./.tina4/context.db", fts5_check=None):
98
+ """``path`` is the on-disk index file (its parent dir is created).
99
+ ``fts5_check`` overrides FTS5 detection (used by tests to exercise the
100
+ graceful-degradation path); defaults to a real probe of this build.
101
+ """
102
+ self.path = str(path)
103
+ self._lock = threading.Lock()
104
+ self.conn = None
105
+ self.root = None # set by index_root; reindex_file relabels against it
106
+ check = fts5_check or _fts5_supported
107
+ self.available = bool(check())
108
+ if not self.available:
109
+ return
110
+ parent = Path(self.path).parent
111
+ if str(parent) not in ("", "."):
112
+ parent.mkdir(parents=True, exist_ok=True)
113
+ # One connection shared across threads (the dev-MCP handler may run on
114
+ # a worker thread) with a lock serializing access — a same-thread-only
115
+ # connection would reject those calls outright.
116
+ self.conn = sqlite3.connect(self.path, check_same_thread=False)
117
+ self._ensure_table()
118
+
119
+ # ── FTS5 availability ───────────────────────────────────────
120
+ @classmethod
121
+ def fts5_available(cls) -> bool:
122
+ """Whether this Python's sqlite3 supports FTS5 (module-level probe)."""
123
+ return _fts5_supported()
124
+
125
+ # ── schema ──────────────────────────────────────────────────
126
+ def _ensure_table(self):
127
+ # `body` holds fold(text) so query/index tokenization is symmetric;
128
+ # `raw` (UNINDEXED) keeps the original text to return as a snippet;
129
+ # `path`/`cid` (UNINDEXED) are metadata used for upsert + citation.
130
+ self.conn.execute(
131
+ "CREATE VIRTUAL TABLE IF NOT EXISTS chunks "
132
+ "USING fts5(cid UNINDEXED, path UNINDEXED, raw UNINDEXED, body)"
133
+ )
134
+ self.conn.commit()
135
+
136
+ def reset(self):
137
+ """Drop and recreate the index (full rebuild starting point)."""
138
+ if not self.available:
139
+ return
140
+ with self._lock:
141
+ self.conn.execute("DROP TABLE IF EXISTS chunks")
142
+ self._ensure_table()
143
+
144
+ # ── indexing ────────────────────────────────────────────────
145
+ @staticmethod
146
+ def _chunks_for(label, text):
147
+ """(index, chunk_text) pairs — code files on def/class boundaries,
148
+ everything else (docs) as prose."""
149
+ ext = os.path.splitext(label)[1].lower()
150
+ special = os.path.basename(label).lower() in SPECIAL_FILES
151
+ if ext in CODE_EXTS or ext in CONFIG_EXTS or special:
152
+ return chunk_code(text, path=label)
153
+ return chunk_text(text)
154
+
155
+ def index_path(self, file, label=None) -> int:
156
+ """UPSERT one file into the index: delete this path's existing chunks,
157
+ re-chunk the current contents, insert. A single file save re-indexes
158
+ just that file (sub-100ms). ``label`` is the stored/citation path
159
+ (defaults to ``str(file)``) and MUST be stable across calls for the
160
+ same file so the delete targets the right rows. Returns rows inserted.
161
+ """
162
+ if not self.available:
163
+ return 0
164
+ stored = str(label) if label is not None else str(file)
165
+ try:
166
+ text = Path(file).read_text(encoding="utf-8", errors="ignore")
167
+ except OSError:
168
+ return 0
169
+ rows = [
170
+ (f"{stored}:{i}", stored, chunk, fold(chunk))
171
+ for i, chunk in self._chunks_for(stored, text)
172
+ ]
173
+ with self._lock:
174
+ self.conn.execute("DELETE FROM chunks WHERE path = ?", (stored,))
175
+ if rows:
176
+ self.conn.executemany(
177
+ "INSERT INTO chunks(cid, path, raw, body) VALUES (?, ?, ?, ?)",
178
+ rows,
179
+ )
180
+ self.conn.commit()
181
+ return len(rows)
182
+
183
+ @staticmethod
184
+ def _eligible(filename) -> bool:
185
+ """True if a file should be indexed (the per-file filter used by both
186
+ index_root and reindex_file). Directory skipping is handled separately."""
187
+ fn = filename.lower()
188
+ if fn.endswith(".min.js"):
189
+ return False
190
+ ext = os.path.splitext(fn)[1]
191
+ return (ext in CODE_EXTS or ext in DOC_EXTS or ext in CONFIG_EXTS
192
+ or fn in SPECIAL_FILES)
193
+
194
+ def index_root(self, root) -> int:
195
+ """Walk ``root``, indexing every eligible file (skips vendor/build/
196
+ runtime dirs). Paths are stored RELATIVE to ``root`` for clean
197
+ citations. Records ``root`` so reindex_file can relabel a changed file
198
+ consistently. Returns the total number of chunks inserted."""
199
+ if not self.available:
200
+ return 0
201
+ root = Path(root).resolve()
202
+ self.root = root
203
+ total = 0
204
+ for dirpath, dirnames, filenames in os.walk(root):
205
+ # prune skip-dirs and dotdirs in place
206
+ dirnames[:] = [d for d in dirnames
207
+ if d not in SKIP_DIRS and not d.startswith(".")]
208
+ for fn in sorted(filenames):
209
+ if not self._eligible(fn):
210
+ continue
211
+ full = Path(dirpath) / fn
212
+ rel = str(full.relative_to(root))
213
+ total += self.index_path(full, label=rel)
214
+ return total
215
+
216
+ def reindex_file(self, changed_path) -> int:
217
+ """Re-index a single changed file into the LIVE index — the hook the dev
218
+ WebSocket reload trigger (POST /__dev/api/reload) calls so code_search
219
+ tracks edits without a rebuild. Resolves ``changed_path`` against the
220
+ indexed root, then: outside root / under a skip-or-dot dir / ineligible
221
+ → skip (-1); deleted → drop its chunks (0); otherwise UPSERT (rows).
222
+ No-op (-1) until index_root has run (nothing to keep fresh yet)."""
223
+ if not self.available or self.root is None:
224
+ return -1
225
+ p = Path(changed_path)
226
+ if not p.is_absolute():
227
+ # the reload trigger reports paths relative to the project root (cwd
228
+ # during `tina4 serve`); the index root may be a subdir like src/.
229
+ p = Path.cwd() / changed_path
230
+ try:
231
+ rel = p.resolve().relative_to(self.root)
232
+ except (ValueError, OSError):
233
+ return -1 # outside the indexed root
234
+ if set(rel.parts) & SKIP_DIRS or any(part.startswith(".") for part in rel.parts[:-1]):
235
+ return -1 # under a skipped / dot dir
236
+ if not self._eligible(rel.name):
237
+ return -1
238
+ stored = str(rel)
239
+ if not p.exists(): # deleted → drop its chunks
240
+ with self._lock:
241
+ self.conn.execute("DELETE FROM chunks WHERE path = ?", (stored,))
242
+ self.conn.commit()
243
+ return 0
244
+ return self.index_path(p, label=stored)
245
+
246
+ # ── query ───────────────────────────────────────────────────
247
+ def _match_expr(self, query):
248
+ """Build the FTS5 MATCH string: each folded query token (plus its
249
+ light stem) as a quoted OR term. Quoting keeps it injection-safe."""
250
+ toks = set()
251
+ for t in terms(query):
252
+ toks.add(t)
253
+ toks.add(light_stem(t))
254
+ toks.discard("")
255
+ if not toks:
256
+ return None, set()
257
+ expr = " OR ".join('"%s"' % t for t in sorted(toks))
258
+ return expr, toks
259
+
260
+ @staticmethod
261
+ def _is_testlike(path):
262
+ s = (path or "").lower()
263
+ return any(p in s for p in ("/test", "test/", "test_", "_test.",
264
+ ".test.", "/example", "example/"))
265
+
266
+ def _defines(self, folded_body, symbols):
267
+ if not symbols:
268
+ return False
269
+ pat = re.compile(
270
+ _DEF_KW + r"\s+(?:" + "|".join(re.escape(s) for s in symbols)
271
+ + r")(?![a-z0-9])")
272
+ return bool(pat.search(folded_body))
273
+
274
+ def search(self, query, k=5):
275
+ """Return the top-``k`` chunks as ``[{path, score, snippet}]``, ranked
276
+ by ``bm25()`` then reordered with two stable, proven passes:
277
+ - source-over-tests: a test that merely mentions a symbol sinks below
278
+ the source that defines it (skipped when the query is about tests);
279
+ - definition-first: a chunk that DEFINES a queried symbol rises above
280
+ chunks that only use it.
281
+ Score is a higher-is-better float (sqlite's bm25 sign flipped).
282
+ """
283
+ if not self.available:
284
+ return []
285
+ expr, _ = self._match_expr(query)
286
+ if expr is None:
287
+ return []
288
+ pool_n = max(k * 3, 15)
289
+ with self._lock:
290
+ rows = self.conn.execute(
291
+ "SELECT path, raw, body, bm25(chunks) AS s FROM chunks "
292
+ "WHERE chunks MATCH ? ORDER BY s LIMIT ?",
293
+ (expr, pool_n),
294
+ ).fetchall()
295
+ if not rows:
296
+ return []
297
+
298
+ # candidate symbol names from the query (drop generic vocab).
299
+ symbols = {t for t in terms(query) if len(t) >= 3 and t not in _DEF_STOP}
300
+ about_tests = "test" in query.lower()
301
+
302
+ def sort_key(item):
303
+ idx, (path, raw, body, s) = item
304
+ is_test = 1 if (not about_tests and self._is_testlike(path)) else 0
305
+ is_def = 0 if self._defines(body, symbols) else 1
306
+ return (is_test, is_def, idx) # bm25 order (idx) breaks ties
307
+
308
+ ordered = sorted(enumerate(rows), key=sort_key)
309
+ out = []
310
+ for _, (path, raw, body, s) in ordered[:k]:
311
+ out.append({
312
+ "path": path,
313
+ "score": round(-float(s), 6), # bm25: more negative = better
314
+ "snippet": self._snippet(raw),
315
+ })
316
+ return out
317
+
318
+ @staticmethod
319
+ def _snippet(raw, limit=280):
320
+ text = (raw or "").strip()
321
+ if len(text) <= limit:
322
+ return text
323
+ return text[:limit].rstrip() + " ..."
324
+
325
+ # ── misc ────────────────────────────────────────────────────
326
+ def count(self) -> int:
327
+ if not self.available:
328
+ return 0
329
+ with self._lock:
330
+ return self.conn.execute("SELECT count(*) FROM chunks").fetchone()[0]
331
+
332
+ def is_empty(self) -> bool:
333
+ return self.count() == 0
334
+
335
+ def close(self):
336
+ if self.conn is not None:
337
+ with self._lock:
338
+ self.conn.close()
339
+ self.conn = None
340
+
341
+
342
+ # ── process-wide shared index ───────────────────────────────────
343
+ # code_search (dev MCP) and the dev-reload reindex hook must share ONE index so
344
+ # a saved file is immediately searchable. Keyed by resolved db path.
345
+ _shared_contexts: dict = {}
346
+
347
+
348
+ def _db_key(db=None) -> str:
349
+ return str((Path(db) if db else Path.cwd() / ".tina4" / "context.db").resolve())
350
+
351
+
352
+ def default_context(root=None, db=None) -> "Context":
353
+ """Get (or create) the process-wide Context at ``db`` (default
354
+ ``<cwd>/.tina4/context.db``). If ``root`` is given and the index is empty,
355
+ builds it once. This is what code_search uses so the reload hook can keep
356
+ the SAME index fresh."""
357
+ key = _db_key(db)
358
+ ctx = _shared_contexts.get(key)
359
+ if ctx is None:
360
+ ctx = Context(key)
361
+ _shared_contexts[key] = ctx
362
+ if root is not None and ctx.available and ctx.is_empty():
363
+ ctx.index_root(root)
364
+ return ctx
365
+
366
+
367
+ def existing_context(db=None):
368
+ """Return the already-created shared Context for ``db`` (or None). Used by
369
+ the reload hook so a file change reindexes an EXISTING index but never
370
+ creates one on its own (nothing to keep fresh until code_search runs)."""
371
+ return _shared_contexts.get(_db_key(db))
@@ -0,0 +1,120 @@
1
+ # Tina4Python
2
+ #
3
+ # Text folding + chunking for the Context subsystem.
4
+ #
5
+ # A thin, idiomatic port of the proven slice of neemee's tokenizer/chunkers
6
+ # (longmem-harness: pipeline.chunk_code / chunk_text, memory_systems.fold).
7
+ # Pure stdlib — ``re``/``unicodedata`` only. Nothing here touches SQLite; it
8
+ # produces the (folded body, raw text) pairs the FTS5 index stores.
9
+
10
+ import re
11
+ import unicodedata
12
+
13
+ # join comma-grouped numbers ("24,601" -> "24601") and split camelCase
14
+ # ("ForeignKeyField" -> "foreign key field") so a query token reaches a
15
+ # code identifier. snake_case already splits for free at the tokenizer.
16
+ _NUM_COMMA = re.compile(r"(?<=\d),(?=\d)")
17
+ _CAMEL = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
18
+ _WORD_RE = re.compile(r"[a-z0-9]+")
19
+
20
+ # Sentence boundary = end punctuation FOLLOWED BY whitespace/EOL, or a newline.
21
+ # NOT a bare '.', which would shred embedded code in prose ("db.fetch()" ->
22
+ # "db. fetch()"). Intra-token dots (method calls, module paths) stay intact.
23
+ _SENT = re.compile(r"(?<=[.!?])\s+|\n+")
24
+
25
+ # Chunk boundaries across languages: python defs/classes/decorators, php/js
26
+ # functions and methods, ts exports/interfaces, php traits, and Object Pascal
27
+ # unit/type/routine headers (Delphi is case-insensitive, accept either case).
28
+ _TOPLEVEL = re.compile(
29
+ r"^(async def |def |class |@\w"
30
+ r"|\s*(?:public |private |protected |static |final |abstract )*function \w"
31
+ r"|(?:export )?(?:default )?(?:abstract )?class "
32
+ r"|interface |trait "
33
+ r"|export (?:const|function|interface|type|async) "
34
+ r"|[Uu]nit |[Pp]rocedure |[Ff]unction |[Cc]onstructor |[Dd]estructor "
35
+ r"|[Tt]ype$)"
36
+ )
37
+
38
+
39
+ def fold(text: str) -> str:
40
+ """Lowercase, strip diacritics, join comma-grouped numbers, split camelCase.
41
+
42
+ Applied symmetrically to the indexed ``body`` and to query tokens so
43
+ matching is consistent: a query for ``field`` reaches ``IntegerField``.
44
+ """
45
+ text = _CAMEL.sub(" ", _NUM_COMMA.sub("", text)).lower()
46
+ return unicodedata.normalize("NFKD", text) \
47
+ .encode("ascii", "ignore").decode("ascii")
48
+
49
+
50
+ def light_stem(token: str) -> str:
51
+ """Fold simple plurals: strip one trailing 's', never from ss/us/is
52
+ endings (class, status, axis). QUERY-side only — so ``fields`` in a
53
+ question also reaches ``Field`` definitions."""
54
+ if len(token) > 3 and token.endswith("s") and not token.endswith(("ss", "us", "is")):
55
+ return token[:-1]
56
+ return token
57
+
58
+
59
+ def terms(text: str):
60
+ """Accent-folded lowercase alphanumeric tokens (digits kept)."""
61
+ return _WORD_RE.findall(fold(text))
62
+
63
+
64
+ def _pack(segments, max_lines):
65
+ """Pack line-segments into chunks of at most ``max_lines`` lines,
66
+ hard-splitting any single oversized segment."""
67
+ chunks, cur = [], []
68
+ for seg in segments:
69
+ while len(seg) > max_lines: # oversized segment: hard split
70
+ if cur:
71
+ chunks.append(cur)
72
+ cur = []
73
+ chunks.append(seg[:max_lines])
74
+ seg = seg[max_lines:]
75
+ if cur and len(cur) + len(seg) > max_lines:
76
+ chunks.append(cur)
77
+ cur = []
78
+ cur += seg
79
+ if cur:
80
+ chunks.append(cur)
81
+ return chunks
82
+
83
+
84
+ def chunk_code(text: str, path: str = "", max_lines: int = 60):
85
+ """Chunk source on top-level def/class/decorator boundaries (sentence
86
+ chunking shreds code). Segments are packed up to ``max_lines``, and every
87
+ chunk starts with a ``# file: <path>`` line so the path's tokens are
88
+ indexed — 'where is the router?' should match core/router.py by name.
89
+
90
+ Returns a list of ``(index, chunk_text)`` pairs.
91
+ """
92
+ lines = text.splitlines()
93
+ bounds = [i for i, ln in enumerate(lines) if _TOPLEVEL.match(ln)]
94
+ if not bounds or bounds[0] != 0:
95
+ bounds = [0] + bounds
96
+ segments = [lines[a:b] for a, b in zip(bounds, bounds[1:] + [len(lines)])]
97
+
98
+ header = f"# file: {path}" if path else None
99
+ out = []
100
+ for i, ch in enumerate(_pack(segments, max_lines)):
101
+ body = "\n".join(([header] if header else []) + ch)
102
+ out.append((i, body))
103
+ return out
104
+
105
+
106
+ def chunk_text(text: str, max_words: int = 350):
107
+ """Chunk prose/docs into sentence-packed windows of at most ``max_words``
108
+ words. Returns a list of ``(index, chunk_text)`` pairs."""
109
+ sents = [s.strip() for s in _SENT.split(text) if s.strip()]
110
+ chunks, cur, cur_words = [], [], 0
111
+ for s in sents:
112
+ w = len(s.split())
113
+ if cur and cur_words + w > max_words:
114
+ chunks.append(" ".join(cur))
115
+ cur, cur_words = [], 0
116
+ cur.append(s)
117
+ cur_words += w
118
+ if cur:
119
+ chunks.append(" ".join(cur))
120
+ return list(enumerate(chunks))
@@ -21,8 +21,10 @@ Generated endpoints per model:
21
21
  PUT /api/{table_name}/{id} — update record by primary key
22
22
  DELETE /api/{table_name}/{id} — delete record by primary key
23
23
 
24
- All write routes (POST/PUT/DELETE) are registered with noauth so they
25
- don't conflict with existing auth defaults. Add auth via middleware.
24
+ Write routes (POST/PUT/DELETE) are SECURE BY DEFAULT they require a valid
25
+ token, matching Tina4's framework-wide default (the router gates writes unless a
26
+ route is @noauth). Pass ``public=True`` to open them explicitly, e.g.
27
+ ``AutoCrud.register(Note, public=True)``.
26
28
  """
27
29
  import importlib
28
30
  import inspect
@@ -66,12 +68,15 @@ class AutoCrud:
66
68
  return example
67
69
 
68
70
  @staticmethod
69
- def register(model_class, prefix: str = "/api"):
71
+ def register(model_class, prefix: str = "/api", public: bool = False):
70
72
  """Register REST endpoints for a single ORM model class.
71
73
 
72
74
  Args:
73
75
  model_class: An ORM subclass with table_name and fields defined.
74
76
  prefix: URL prefix for the generated routes (default "/api").
77
+ public: If True, the write routes (POST/PUT/DELETE) are OPEN (no auth).
78
+ Default False keeps them **secure-by-default** — a valid token is
79
+ required, matching the framework's write-gating.
75
80
 
76
81
  Returns:
77
82
  List of dicts describing the generated routes.
@@ -168,7 +173,8 @@ class AutoCrud:
168
173
 
169
174
  create_handler.__name__ = f"autocrud_create_{table}"
170
175
  create_handler.__qualname__ = f"autocrud_create_{table}"
171
- create_handler._noauth = True
176
+ if public: # secure-by-default; opt-in to public writes
177
+ create_handler._noauth = True
172
178
  create_handler._swagger_summary = f"Create {pretty_name}"
173
179
  create_handler._swagger_tags = [table]
174
180
  create_handler._swagger_example = example_body
@@ -201,7 +207,8 @@ class AutoCrud:
201
207
 
202
208
  update_handler.__name__ = f"autocrud_update_{table}"
203
209
  update_handler.__qualname__ = f"autocrud_update_{table}"
204
- update_handler._noauth = True
210
+ if public:
211
+ update_handler._noauth = True
205
212
  update_handler._swagger_summary = f"Update {pretty_name}"
206
213
  update_handler._swagger_tags = [table]
207
214
  update_handler._swagger_example = example_body
@@ -224,7 +231,8 @@ class AutoCrud:
224
231
 
225
232
  delete_handler.__name__ = f"autocrud_delete_{table}"
226
233
  delete_handler.__qualname__ = f"autocrud_delete_{table}"
227
- delete_handler._noauth = True
234
+ if public:
235
+ delete_handler._noauth = True
228
236
  delete_handler._swagger_summary = f"Delete {pretty_name}"
229
237
  delete_handler._swagger_tags = [table]
230
238
  Router.add("DELETE", f"{base_path}/{{id}}", delete_handler)
@@ -237,7 +245,7 @@ class AutoCrud:
237
245
  return generated
238
246
 
239
247
  @staticmethod
240
- def discover(models_dir: str = "src/orm", prefix: str = "/api"):
248
+ def discover(models_dir: str = "src/orm", prefix: str = "/api", public: bool = False):
241
249
  """Auto-discover all ORM models in a directory and register CRUD routes.
242
250
 
243
251
  Scans .py files in the given directory, imports them, and registers
@@ -308,7 +316,7 @@ class AutoCrud:
308
316
  and attr._get_table() # Has a table name
309
317
  and attr._get_table() not in AutoCrud._registered
310
318
  ):
311
- AutoCrud.register(attr, prefix=prefix)
319
+ AutoCrud.register(attr, prefix=prefix, public=public)
312
320
  discovered.append(attr.__name__)
313
321
 
314
322
  if discovered:
@@ -2000,6 +2000,19 @@ async def _api_reload(request, response):
2000
2000
  except Exception as e:
2001
2001
  Log.error(f"Re-discover on reload failed: {e}")
2002
2002
 
2003
+ # Keep the code Context index LIVE on the same WebSocket-reload trigger:
2004
+ # reindex just the changed file (UPSERT) so the dev-MCP code_search reflects
2005
+ # the edit immediately. Only touches an already-built index (existing_context
2006
+ # never creates one); guarded so a context failure never breaks the reload.
2007
+ try:
2008
+ if _reload_file[0]:
2009
+ from tina4_python.context import existing_context
2010
+ _ctx = existing_context()
2011
+ if _ctx is not None:
2012
+ _ctx.reindex_file(_reload_file[0])
2013
+ except Exception as e:
2014
+ Log.error(f"Context reindex on reload failed: {e}")
2015
+
2003
2016
  # WebSocket-primary reload: push an instant message to every browser
2004
2017
  # connected on /__dev_reload. The toolbar client (and the dev-admin
2005
2018
  # dashboard) act on this immediately — the mtime poll above is only a