tina4-python 3.13.119__tar.gz → 3.13.121__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 (170) hide show
  1. {tina4_python-3.13.119 → tina4_python-3.13.121}/PKG-INFO +1 -1
  2. {tina4_python-3.13.119 → tina4_python-3.13.121}/pyproject.toml +1 -1
  3. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/cli/__init__.py +300 -28
  5. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mcp/tools.py +93 -15
  6. {tina4_python-3.13.119 → tina4_python-3.13.121}/.gitignore +0 -0
  7. {tina4_python-3.13.119 → tina4_python-3.13.121}/README.md +0 -0
  8. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/_import_helper.py +0 -0
  12. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/ai/client.py +0 -0
  14. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/api/__init__.py +0 -0
  15. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/auth/__init__.py +0 -0
  16. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/cache/__init__.py +0 -0
  17. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/context/__init__.py +0 -0
  19. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/context/chunker.py +0 -0
  20. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/port_takeover.py +0 -0
  26. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/rate_limiter.py +0 -0
  27. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/request.py +0 -0
  28. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/response.py +0 -0
  29. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/router.py +0 -0
  30. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/core/server.py +0 -0
  31. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/crud/__init__.py +0 -0
  32. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/__init__.py +0 -0
  33. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/adapter.py +0 -0
  34. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/connection.py +0 -0
  35. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/database_url.py +0 -0
  36. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/firebird.py +0 -0
  37. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/mongodb.py +0 -0
  38. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/mssql.py +0 -0
  39. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/mysql.py +0 -0
  40. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/odbc.py +0 -0
  41. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/postgres.py +0 -0
  42. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/sql_translator.py +0 -0
  43. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/database/sqlite.py +0 -0
  44. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/debug/__init__.py +0 -0
  45. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/debug/error_overlay.py +0 -0
  46. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/__init__.py +0 -0
  47. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/metrics.py +0 -0
  48. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/plan.py +0 -0
  49. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dev_admin/project_index.py +0 -0
  50. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/docs.py +0 -0
  51. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/docstore/__init__.py +0 -0
  52. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/dotenv/__init__.py +0 -0
  53. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/env.py +0 -0
  54. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/FROND.md +0 -0
  55. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/__init__.py +0 -0
  56. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/compiler.py +0 -0
  57. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/engine.py +0 -0
  58. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/frond/parser.py +0 -0
  59. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/auth/meta.json +0 -0
  60. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  61. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/database/meta.json +0 -0
  62. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  63. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/error-overlay/meta.json +0 -0
  64. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  65. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/orm/meta.json +0 -0
  66. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  67. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  68. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/queue/meta.json +0 -0
  69. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  70. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/rest-api/meta.json +0 -0
  71. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  72. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/templates/meta.json +0 -0
  73. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  74. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  75. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/__init__.py +0 -0
  76. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapter.py +0 -0
  77. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/__init__.py +0 -0
  78. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/arango.py +0 -0
  79. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/bolt.py +0 -0
  80. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/adapters/ultipa.py +0 -0
  81. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graph/graph_url.py +0 -0
  82. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/graphql/__init__.py +0 -0
  83. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/i18n/__init__.py +0 -0
  84. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mcp/__init__.py +0 -0
  85. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mcp/protocol.py +0 -0
  86. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/messenger/__init__.py +0 -0
  87. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/migration/__init__.py +0 -0
  88. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/migration/runner.py +0 -0
  89. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mqtt/__init__.py +0 -0
  90. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/mqtt/message.py +0 -0
  91. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/__init__.py +0 -0
  92. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/fields.py +0 -0
  93. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/model.py +0 -0
  94. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/orm/point.py +0 -0
  95. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/__feedback/widget.js +0 -0
  96. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/css/tina4.css +0 -0
  97. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/css/tina4.min.css +0 -0
  98. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/favicon.ico +0 -0
  99. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/images/logo.svg +0 -0
  100. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  101. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/frond.js +0 -0
  102. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/frond.min.js +0 -0
  103. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  104. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/tina4.min.js +0 -0
  105. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/js/tina4js.min.js +0 -0
  106. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/swagger/index.html +0 -0
  107. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  108. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/query_builder/__init__.py +0 -0
  109. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/__init__.py +0 -0
  110. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/amqp_url.py +0 -0
  111. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/job.py +0 -0
  112. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/kafka_backend.py +0 -0
  113. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/lite_backend.py +0 -0
  114. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/mongo_backend.py +0 -0
  115. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue/rabbitmq_backend.py +0 -0
  116. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/__init__.py +0 -0
  117. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/kafka_backend.py +0 -0
  118. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/mongo_backend.py +0 -0
  119. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  120. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/__init__.py +0 -0
  121. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Attachment.py +0 -0
  122. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Channel.py +0 -0
  123. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/ChannelMember.py +0 -0
  124. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Message.py +0 -0
  125. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/Workspace.py +0 -0
  126. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/models/__init__.py +0 -0
  127. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/realtime/storage.py +0 -0
  128. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/memcached_handler.py +0 -0
  133. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  134. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/redis_handler.py +0 -0
  135. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/session_handlers/valkey_handler.py +0 -0
  136. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/sso.py +0 -0
  137. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/swagger/__init__.py +0 -0
  138. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/components/crud.twig +0 -0
  139. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  140. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  141. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/python/Dockerfile +0 -0
  142. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  143. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/302.twig +0 -0
  144. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/401.twig +0 -0
  145. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/403.twig +0 -0
  146. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/404.twig +0 -0
  147. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/500.twig +0 -0
  148. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/502.twig +0 -0
  149. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/503.twig +0 -0
  150. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/errors/base.twig +0 -0
  151. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/frontend/README.md +0 -0
  152. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/templates/readme.md +0 -0
  153. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/test/__init__.py +0 -0
  154. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/test_client/__init__.py +0 -0
  155. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/validator/__init__.py +0 -0
  168. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/websocket/__init__.py +0 -0
  169. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/websocket/backplane.py +0 -0
  170. {tina4_python-3.13.119 → tina4_python-3.13.121}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tina4-python
3
- Version: 3.13.119
3
+ Version: 3.13.121
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.119"
3
+ version = "3.13.121"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
51
51
  #
52
52
  # test_version_constant.py now asserts this literal equals the pyproject
53
53
  # version, so the release bump cannot leave it behind again.
54
- return "3.13.119"
54
+ return "3.13.121"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -732,14 +732,35 @@ def _migrate(args):
732
732
 
733
733
 
734
734
  def _migrate_create(args):
735
- """Create a new migration file."""
735
+ """Create a new migration file.
736
+
737
+ Thin delegation to `_emit_resolution("migration", ...)` — the same code
738
+ path `tina4python generate migration <desc>` takes — so both CLI surfaces
739
+ emit the SAME ADR-0063 `generate_v1_1` envelope, the SAME `tina4:edit`
740
+ markers in the file, and the SAME next-steps block. The only intentional
741
+ difference is that `migrate:create` never co-emits a test file (a private
742
+ `_no_test` marker on the flags dict suppresses the test-emit path in
743
+ `_gen_migration` and drops `test_paths[]` in the resolution), preserving
744
+ the "just a migration, no test" contract of `migrate:create`. Neither
745
+ surface is deprecated; both stay first-class producers of the envelope.
746
+
747
+ The Python API `create_migration()` in `tina4_python.migration.runner` is
748
+ unchanged and still callable directly.
749
+ """
736
750
  if not args:
737
751
  print("Usage: tina4python migrate:create <description>")
738
752
  sys.exit(1)
739
- from tina4_python.migration import create_migration
740
- desc = " ".join(args)
741
- path = create_migration(desc, "migrations")
742
- print(f"Created: {path}")
753
+ # Split flags vs positional so `--json` / `--dry-run` flow through the
754
+ # envelope emitter (bare word args form the description).
755
+ flags, positional = _parse_flags(list(args))
756
+ if not positional:
757
+ print("Usage: tina4python migrate:create <description>")
758
+ sys.exit(1)
759
+ desc = " ".join(positional)
760
+ # Private marker read by _gen_migration + _resolve_generation to suppress
761
+ # the co-emitted test — see the `migrate:create` contract above.
762
+ flags["_no_test"] = True
763
+ _emit_resolution("migration", desc, flags)
743
764
 
744
765
 
745
766
  def _migrate_rollback(args):
@@ -986,7 +1007,24 @@ _RESOLUTION_TARGETS = {"model", "route", "migration", "middleware"}
986
1007
 
987
1008
  # Envelope schema version. Bump when the SHAPE changes; adding an optional
988
1009
  # key on the resolution object is not a break.
989
- _RESOLUTION_ENVELOPE_VERSION = "1"
1010
+ #
1011
+ # 1.1 (ADR-0063 Wave 1, 3.13.120): additive keys `edit_hints[]` and `next[]`
1012
+ # on `resolution`, plus `test_paths[]` (already emitted since v1) is now
1013
+ # surfaced in the human stderr block too. Every existing generator template
1014
+ # bakes `tina4:edit` markers at first-edit spots so a caller sees exactly
1015
+ # where to point a follow-up patch. Old envelope keys are preserved; a v1
1016
+ # consumer sees the v1 shape intact and simply ignores the new keys.
1017
+ _RESOLUTION_ENVELOPE_VERSION = "1.1"
1018
+ _RESOLUTION_ENVELOPE_NAME = "generate_v1_1"
1019
+
1020
+ # Marker syntax the generator templates bake in at first-edit spots — accepts
1021
+ # a Python (`#`), SQL (`--`), or Twig (`{#`) comment prefix, followed by a
1022
+ # short actionable label. The label ends at end-of-line or a closing Twig `#}`.
1023
+ # NEVER matches inside a string literal — the compiler regex is a plain textual
1024
+ # scan of freshly-written source, but no template contains a matching literal.
1025
+ _EDIT_MARKER_RE = re.compile(
1026
+ r"(?:#|--|\{#)\s*tina4:edit\s+([^\n#]+?)\s*(?:#\})?\s*$"
1027
+ )
990
1028
 
991
1029
 
992
1030
  def _input_fields(flags: dict):
@@ -1002,6 +1040,118 @@ def _input_fields(flags: dict):
1002
1040
  return value
1003
1041
 
1004
1042
 
1043
+ def _scan_edit_hints(root: Path, rel_paths: list[str]) -> list[dict]:
1044
+ """Grep freshly-written files for `tina4:edit` markers (envelope v1.1).
1045
+
1046
+ Reads each `rel_paths` entry under `root`, walks its lines, and returns
1047
+ `[{file, line, label}]` — one entry per marker, in file+line order.
1048
+
1049
+ A missing file is silently skipped: the caller wires this into the
1050
+ envelope path where `actions_taken` is the source of truth for what
1051
+ landed. The scan is text-only — no imports, no parsing — so an edited
1052
+ template that fails to run still yields honest hints.
1053
+ """
1054
+ hints: list[dict] = []
1055
+ for rel in rel_paths:
1056
+ f = root / rel
1057
+ if not f.is_file():
1058
+ continue
1059
+ try:
1060
+ text = f.read_text(encoding="utf-8")
1061
+ except OSError:
1062
+ continue
1063
+ for lineno, line in enumerate(text.splitlines(), start=1):
1064
+ match = _EDIT_MARKER_RE.search(line)
1065
+ if match:
1066
+ hints.append({
1067
+ "file": rel,
1068
+ "line": lineno,
1069
+ "label": match.group(1).strip(),
1070
+ })
1071
+ return hints
1072
+
1073
+
1074
+ def _hint_paths_from_resolution(target: str, resolution: dict) -> list[str]:
1075
+ """The set of paths the caller should scan for `tina4:edit` markers.
1076
+
1077
+ Every path the generator ACTUALLY writes is worth scanning — the model
1078
+ file, the matching migration + down, the middleware file, the co-emitted
1079
+ test — because a marker in any of them names a first-edit spot the
1080
+ developer will hit. Order is stable: primary file, then migration + down,
1081
+ then any test paths declared in the envelope. The hints themselves land
1082
+ in file+line order regardless.
1083
+ """
1084
+ paths: list[str] = []
1085
+ primary = resolution.get("file_path")
1086
+ if primary:
1087
+ paths.append(primary)
1088
+
1089
+ # Model + migration composite: the migration is written by _gen_migration
1090
+ # under the model's own generator, so its markers deserve to surface too.
1091
+ if target == "model" and resolution.get("migration_path"):
1092
+ migration = resolution["migration_path"]
1093
+ paths.append(migration)
1094
+ if migration.endswith(".sql"):
1095
+ paths.append(migration[:-len(".sql")] + ".down.sql")
1096
+
1097
+ # A bare migration also writes its own .down.sql.
1098
+ if target == "migration" and primary and primary.endswith(".sql"):
1099
+ paths.append(primary[:-len(".sql")] + ".down.sql")
1100
+
1101
+ # Co-emitted tests carry their own markers (see the test template).
1102
+ for test_path in resolution.get("test_paths", []):
1103
+ if test_path not in paths:
1104
+ paths.append(test_path)
1105
+ return paths
1106
+
1107
+
1108
+ def _next_steps_for(target: str, name: str, resolution: dict, flags: dict) -> list[str]:
1109
+ """Curated actionable next-steps a caller should see after generation.
1110
+
1111
+ Kept short and concrete: what to edit, what to run, how to try it. The
1112
+ entries are per-target because a generic "next steps" list dilutes into
1113
+ noise — a route's next step is `curl`, a middleware's next step is
1114
+ `@middleware(...)` on a real route.
1115
+
1116
+ Placeholders are resolved from the same shape the envelope already
1117
+ reports (class_name, table_name, routes), so a caller can trust the
1118
+ strings match what actually landed on disk.
1119
+ """
1120
+ if target == "model":
1121
+ table = resolution.get("table_name") or _to_table(name)
1122
+ return [
1123
+ f"Edit src/orm/{name}.py to add fields beyond the default `name`",
1124
+ "Apply the migration: tina4 migrate",
1125
+ f"Scaffold CRUD around it: tina4 generate crud {name} --skip-model",
1126
+ f"Try it: tina4 serve -> curl http://localhost:7146/api/{table}",
1127
+ ]
1128
+
1129
+ if target == "route":
1130
+ route_path = name.lstrip("/")
1131
+ first_url = (resolution.get("routes") or [f"/api/{route_path}"])[0]
1132
+ return [
1133
+ f"Edit src/routes/{route_path}.py to customise the handlers",
1134
+ "Start the server: tina4 serve",
1135
+ f"Try it: curl http://localhost:7146{first_url}",
1136
+ ]
1137
+
1138
+ if target == "migration":
1139
+ return [
1140
+ "Apply it: tina4 migrate",
1141
+ "Roll back last batch: tina4 migrate --rollback",
1142
+ "Inspect the pending: tina4 migrate --status",
1143
+ ]
1144
+
1145
+ if target == "middleware":
1146
+ snake = _to_snake(name)
1147
+ return [
1148
+ f"Register it on a route with @middleware({name}) in src/routes/",
1149
+ f"Run the co-emitted test: .venv/bin/python -m pytest tests/test_{snake}.py",
1150
+ ]
1151
+
1152
+ return []
1153
+
1154
+
1005
1155
  def _resolve_generation(target: str, name: str, flags: dict,
1006
1156
  timestamp: str | None = None) -> dict:
1007
1157
  """Compute the resolution mapping for a generate call. Pure — no writes.
@@ -1060,6 +1210,10 @@ def _resolve_generation(target: str, name: str, flags: dict,
1060
1210
  table = _to_snake(table)
1061
1211
  filename = f"{ts}_{name}.sql"
1062
1212
  is_create = name.startswith("create_")
1213
+ # Private `_no_test` marker (set by `migrate:create`, which is a
1214
+ # single-file operation): drop `test_paths[]` so the envelope
1215
+ # matches the on-disk effect. `_gen_migration` reads the same flag.
1216
+ want_test = is_create and not flags.get("_no_test")
1063
1217
  return {
1064
1218
  "class_name": None,
1065
1219
  "table_name": table,
@@ -1067,7 +1221,7 @@ def _resolve_generation(target: str, name: str, flags: dict,
1067
1221
  "migration_path": f"migrations/{filename}",
1068
1222
  "transformations": [],
1069
1223
  "routes": [],
1070
- "test_paths": [f"tests/test_{table}_migration.py"] if is_create else [],
1224
+ "test_paths": [f"tests/test_{table}_migration.py"] if want_test else [],
1071
1225
  }
1072
1226
 
1073
1227
  if target == "middleware":
@@ -1112,12 +1266,19 @@ def _actions_from_resolution(target: str, resolution: dict) -> list[str]:
1112
1266
 
1113
1267
 
1114
1268
  def _format_resolution_block(name: str, target: str, resolution: dict,
1115
- dry_run: bool = False) -> str:
1269
+ dry_run: bool = False,
1270
+ edit_hints: list[dict] | None = None,
1271
+ next_steps: list[str] | None = None) -> str:
1116
1272
  """Render the human-readable resolution block for stderr on bare calls.
1117
1273
 
1118
1274
  Same shape as the plan / ADR-0062 example. A generator with no route/table
1119
1275
  footprint (middleware, route without a matching table) simply omits the
1120
1276
  lines that don't apply — the block is honest about what was chosen.
1277
+
1278
+ ADR-0063 (envelope v1.1) also surfaces `test_paths[]` (was in the envelope
1279
+ since v1, never printed), plus `edit_hints[]` under "Edit these lines:"
1280
+ and `next_steps[]` under "Next:". Each section omits when empty so the
1281
+ block never grows a header without content beneath it.
1121
1282
  """
1122
1283
  suffix = " (dry-run — nothing written)" if dry_run else ""
1123
1284
  lines = [f"Generated {target} {name}{suffix}"]
@@ -1145,6 +1306,13 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
1145
1306
  if target != "migration" and resolution.get("migration_path"):
1146
1307
  lines.append(f" migration {resolution['migration_path']}")
1147
1308
 
1309
+ # ADR-0063: surface test_paths[] in the human block too (it has been in the
1310
+ # envelope since v1 but only the JSON path printed it). Skipped when empty
1311
+ # so a migration without a test file doesn't grow an empty "tests" row.
1312
+ test_paths = resolution.get("test_paths") or []
1313
+ if test_paths:
1314
+ lines.append(f" tests {', '.join(test_paths)}")
1315
+
1148
1316
  if pluralize:
1149
1317
  lines.append("")
1150
1318
  lines.append(f" To keep the raw name '{pluralize['from']}' as the table:")
@@ -1154,21 +1322,47 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
1154
1322
  "(opt-in, ADR-0062 forthcoming)"
1155
1323
  )
1156
1324
 
1325
+ # ADR-0063 additive sections. Each renders only when there is content, so
1326
+ # a target with no markers or no next-steps stays as tight as before.
1327
+ if edit_hints:
1328
+ lines.append("")
1329
+ lines.append("Edit these lines:")
1330
+ # Left-pad the file:line column so labels align — makes scanning easier
1331
+ # when there are three or four hints of different path lengths.
1332
+ addrs = [f"{h['file']}:{h['line']}" for h in edit_hints]
1333
+ width = max(len(a) for a in addrs)
1334
+ for addr, hint in zip(addrs, edit_hints):
1335
+ lines.append(f" {addr:<{width}} {hint['label']}")
1336
+
1337
+ if next_steps:
1338
+ lines.append("")
1339
+ lines.append("Next:")
1340
+ for i, step in enumerate(next_steps, start=1):
1341
+ lines.append(f" {i}. {step}")
1342
+
1157
1343
  return "\n".join(lines) + "\n"
1158
1344
 
1159
1345
 
1160
1346
  def _emit_resolution(target: str, name: str, flags: dict) -> None:
1161
1347
  """Handle the --json / --dry-run / bare paths for the 4 wired generators.
1162
1348
 
1163
- Contract shared by every language (ADR-0062):
1349
+ Contract shared by every language (ADR-0062, extended by ADR-0063):
1164
1350
  - --json → envelope on stdout; nothing else on stdout.
1165
1351
  - --dry-run → skip every write; envelope with dry_run=true.
1166
1352
  - --json + --dry-run → both compose (envelope with actions_taken=[]).
1167
1353
  - bare → normal write, then human resolution block on stderr.
1354
+
1355
+ Envelope v1.1 (ADR-0063) additively surfaces `resolution.edit_hints[]`
1356
+ (one per `tina4:edit` marker found in freshly-written files) and
1357
+ `resolution.next[]` (curated actionable next-steps per verb). The
1358
+ hints are scanned from disk on the write paths; on --dry-run the
1359
+ generator runs in a throw-away tmpdir so the same scan gives the
1360
+ caller a real preview without touching the working tree.
1168
1361
  """
1169
1362
  import contextlib
1170
1363
  import io
1171
1364
  import json as _json
1365
+ import tempfile
1172
1366
 
1173
1367
  dry_run = bool(flags.get("dry-run"))
1174
1368
  want_json = bool(flags.get("json"))
@@ -1178,21 +1372,55 @@ def _emit_resolution(target: str, name: str, flags: dict) -> None:
1178
1372
  timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
1179
1373
  resolution = _resolve_generation(target, name, flags, timestamp=timestamp)
1180
1374
 
1375
+ handler = GENERATORS[target]["handler"]
1376
+ generator_flags = dict(flags)
1377
+ generator_flags["_timestamp"] = timestamp
1378
+ # `_gen_*` handlers read the primary flags; `--dry-run` / `--json` are
1379
+ # envelope-only concerns, they must not leak into the underlying writer.
1380
+ generator_flags.pop("dry-run", None)
1381
+ generator_flags.pop("json", None)
1382
+
1181
1383
  actions_taken: list[str] = []
1182
- if not dry_run:
1183
- # Thread the locked-in timestamp through the underlying generator,
1184
- # then run it. When --json we swallow the generator's stdout so the
1384
+ edit_hints: list[dict] = []
1385
+
1386
+ hint_paths = _hint_paths_from_resolution(target, resolution)
1387
+
1388
+ if dry_run:
1389
+ # Render into a throw-away tree so the scan sees the SAME bytes the
1390
+ # writer would drop on disk. The envelope's paths still point at the
1391
+ # real project paths (unchanged by the tmpdir chdir) — the caller
1392
+ # can trust `edit_hints[].file` and `resolution.file_path` share a
1393
+ # coordinate space.
1394
+ with tempfile.TemporaryDirectory() as tmp:
1395
+ original_cwd = os.getcwd()
1396
+ os.chdir(tmp)
1397
+ try:
1398
+ # Suppress the generator's own "Created X" lines — this is a
1399
+ # dry-run, and its "creations" happen in a tmpdir the caller
1400
+ # will never see. `--json` also swallows stdout on the real
1401
+ # write path; this keeps the two paths symmetrical.
1402
+ with contextlib.redirect_stdout(io.StringIO()):
1403
+ handler(name, generator_flags)
1404
+ edit_hints = _scan_edit_hints(Path(tmp), hint_paths)
1405
+ finally:
1406
+ os.chdir(original_cwd)
1407
+ else:
1408
+ # Real write. When --json we swallow the generator's stdout so the
1185
1409
  # JSON envelope is the SOLE thing on stdout; otherwise we let its
1186
1410
  # "Created X" lines through as before.
1187
- generator_flags = dict(flags)
1188
- generator_flags["_timestamp"] = timestamp
1189
- handler = GENERATORS[target]["handler"]
1190
-
1191
1411
  stdout_sink = io.StringIO() if want_json else sys.stdout
1192
1412
  with contextlib.redirect_stdout(stdout_sink):
1193
1413
  handler(name, generator_flags)
1194
1414
 
1195
1415
  actions_taken = _actions_from_resolution(target, resolution)
1416
+ edit_hints = _scan_edit_hints(Path.cwd(), hint_paths)
1417
+
1418
+ # ADR-0063 additive envelope keys — computed once and reused by the JSON
1419
+ # and human paths so what the caller sees on stderr matches the envelope
1420
+ # byte-for-byte.
1421
+ next_steps = _next_steps_for(target, name, resolution, flags)
1422
+ resolution["edit_hints"] = edit_hints
1423
+ resolution["next"] = next_steps
1196
1424
 
1197
1425
  envelope = {
1198
1426
  "command": "generate",
@@ -1209,8 +1437,12 @@ def _emit_resolution(target: str, name: str, flags: dict) -> None:
1209
1437
  # Human resolution block always lands on STDERR (per plan): stdout
1210
1438
  # keeps whatever the generator printed on the bare path, and the
1211
1439
  # block never contends with a caller parsing stdout.
1212
- sys.stderr.write(_format_resolution_block(name, target, resolution,
1213
- dry_run=dry_run))
1440
+ sys.stderr.write(_format_resolution_block(
1441
+ name, target, resolution,
1442
+ dry_run=dry_run,
1443
+ edit_hints=edit_hints,
1444
+ next_steps=next_steps,
1445
+ ))
1214
1446
 
1215
1447
 
1216
1448
  # ── Generate (rich scaffolding) ───────────────────────────────────────
@@ -1294,11 +1526,18 @@ def _gen_model(name: str, flags: dict, *, emit_test: bool = True):
1294
1526
 
1295
1527
  imports = ", ".join(sorted(used_types))
1296
1528
 
1297
- # Build field lines
1298
- field_lines = [f" id = IntegerField(primary_key=True, auto_increment=True)"]
1529
+ # Build field lines. Bracketed by two `tina4:edit` markers (ADR-0063) so
1530
+ # a first-time user sees WHERE to add columns and relationships — the
1531
+ # `id` and `created_at` lines are framework-managed, the block between
1532
+ # is theirs to grow.
1533
+ field_lines = [
1534
+ " id = IntegerField(primary_key=True, auto_increment=True)",
1535
+ " # tina4:edit add fields here",
1536
+ ]
1299
1537
  for fname, ftype in fields:
1300
1538
  info = FIELD_TYPE_MAP.get(ftype, FIELD_TYPE_MAP["string"])
1301
1539
  field_lines.append(f" {fname} = {info['orm']}()")
1540
+ field_lines.append(" # tina4:edit add relationships here (e.g. author = ForeignKeyField(to=Author))")
1302
1541
  field_lines.append(" created_at = DateTimeField()")
1303
1542
 
1304
1543
  # Write model file
@@ -1395,6 +1634,7 @@ async def list_{route_path}(request, response):
1395
1634
  per_page = int(request.query.get("per_page", 20))
1396
1635
  offset = (page - 1) * per_page
1397
1636
  records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
1637
+ # tina4:edit customise the list projection (filters, ordering, fields)
1398
1638
  return response({{
1399
1639
  "records": [r.to_dict() for r in records],
1400
1640
  "count": total,
@@ -1588,6 +1828,7 @@ def _gen_crud(name: str, flags: dict):
1588
1828
  '{% block content %}\n'
1589
1829
  '<div class="container mt-4">\n'
1590
1830
  f' <h1>{name}s</h1>\n'
1831
+ ' {# tina4:edit restrict fields exposed to the API here #}\n'
1591
1832
  ' <table class="table">\n'
1592
1833
  ' <thead>\n'
1593
1834
  ' <tr>\n'
@@ -1646,6 +1887,12 @@ def _gen_migration(name: str, flags: dict = None, *,
1646
1887
  # snapshotted timestamp; if it was supplied via the private `_timestamp`
1647
1888
  # flag, honour it so envelope and disk agree byte-for-byte.
1648
1889
  timestamp = flags.get("_timestamp") or now.strftime("%Y%m%d%H%M%S")
1890
+ # Private `_no_test` marker (set by `migrate:create`, which is a
1891
+ # single-file operation): suppress the co-emitted test regardless of the
1892
+ # keyword default. `_resolve_generation` reads the same marker so the
1893
+ # envelope's `test_paths[]` and the on-disk effect stay in agreement.
1894
+ if flags.get("_no_test"):
1895
+ emit_test = False
1649
1896
  target = Path("migrations")
1650
1897
  target.mkdir(parents=True, exist_ok=True)
1651
1898
 
@@ -1671,11 +1918,21 @@ def _gen_migration(name: str, flags: dict = None, *,
1671
1918
  col_lines.append(f" {fname} {info['sql']}{default}")
1672
1919
  col_lines.append(" created_at TEXT DEFAULT CURRENT_TIMESTAMP")
1673
1920
 
1674
- up_sql = f"CREATE TABLE IF NOT EXISTS {table} (\n" + ",\n".join(col_lines) + "\n);"
1921
+ # ADR-0063: `tina4:edit` marker between the last column and the closing
1922
+ # paren, placed OUTSIDE the comma-joined column list so the SQL stays
1923
+ # valid. Points a first-time user at where to add columns beyond the
1924
+ # framework-managed id + created_at.
1925
+ up_sql = (
1926
+ f"CREATE TABLE IF NOT EXISTS {table} (\n"
1927
+ + ",\n".join(col_lines)
1928
+ + "\n -- tina4:edit add columns beyond id + created_at\n);"
1929
+ )
1675
1930
  down_sql = f"DROP TABLE IF EXISTS {table};"
1676
1931
  else:
1677
- up_sql = f"-- Write your UP migration SQL here\n-- Example: ALTER TABLE {table} ADD COLUMN new_col TEXT DEFAULT '';"
1678
- down_sql = f"-- Write your DOWN rollback SQL here\n-- Example: ALTER TABLE {table} DROP COLUMN new_col;"
1932
+ up_sql = (f"-- tina4:edit write the UP migration SQL\n"
1933
+ f"-- Example: ALTER TABLE {table} ADD COLUMN new_col TEXT DEFAULT '';")
1934
+ down_sql = (f"-- tina4:edit write the DOWN rollback SQL\n"
1935
+ f"-- Example: ALTER TABLE {table} DROP COLUMN new_col;")
1679
1936
 
1680
1937
  content = (
1681
1938
  f"-- Migration: {name}\n"
@@ -1739,12 +1996,14 @@ class {name}:
1739
1996
  Return (request, response) to continue, or
1740
1997
  return (request, response("error", 401)) to block.
1741
1998
  """
1999
+ # tina4:edit guard the request here (auth check, rate limit, headers)
1742
2000
  Log.info(f"{name}: {{request.method}} {{request.url}}")
1743
2001
  return request, response
1744
2002
 
1745
2003
  @staticmethod
1746
2004
  def after_{snake}(request, response):
1747
2005
  """Runs after the route handler."""
2006
+ # tina4:edit inject headers or audit the response here
1748
2007
  return request, response
1749
2008
  '''
1750
2009
  path.write_text(content, encoding="utf-8")
@@ -1861,7 +2120,7 @@ class Test{model}:
1861
2120
 
1862
2121
  def test_list_{snake}(self):
1863
2122
  """Test listing {snake}."""
1864
- # TODO: implement
2123
+ # tina4:edit add assertions here (real DB, real request — no mocks)
1865
2124
  assert True
1866
2125
 
1867
2126
  def test_get_{singular}(self):
@@ -1902,6 +2161,7 @@ class Test{name.title().replace("_", "")}:
1902
2161
 
1903
2162
  def test_example(self):
1904
2163
  """Example test — replace with real tests."""
2164
+ # tina4:edit add assertions here (real DB, real request — no mocks)
1905
2165
  assert True
1906
2166
  '''
1907
2167
 
@@ -2463,6 +2723,7 @@ def _gen_service(name: str, flags: dict = None):
2463
2723
  ' """The scheduled task body. `context` is a ServiceContext with\n'
2464
2724
  " .name, .stop_event and .log; a daemon-style task would loop until\n"
2465
2725
  ' context.stop_event.is_set()."""\n'
2726
+ " # tina4:edit main service loop here\n"
2466
2727
  "__BODY__"
2467
2728
  "\n\n"
2468
2729
  "def register(runner: ServiceRunner) -> None:\n"
@@ -2534,6 +2795,7 @@ def _gen_queue(name: str, flags: dict = None):
2534
2795
  "\n"
2535
2796
  "def handle___SLUG__(data: dict):\n"
2536
2797
  ' """Process ONE __TOPIC__ job payload (`data` is Job.data — the pushed dict)."""\n'
2798
+ " # tina4:edit handle the job payload here\n"
2537
2799
  "__BODY__"
2538
2800
  "\n\n"
2539
2801
  "def consume___SLUG__(context=None):\n"
@@ -2612,6 +2874,7 @@ def _gen_validator(name: str, flags: dict = None):
2612
2874
  ' return response.error("VALIDATION_FAILED", v.errors()[0]["message"], 400)\n'
2613
2875
  ' """\n'
2614
2876
  " validator = Validator(data)\n"
2877
+ " # tina4:edit add validation rules here\n"
2615
2878
  "__BODY__"
2616
2879
  " return validator\n"
2617
2880
  )
@@ -2664,6 +2927,7 @@ def _gen_seeder(name: str, flags: dict = None):
2664
2927
  " need a specific shape here. Each callable receives a FakeData instance:\n"
2665
2928
  ' return {"email": lambda fake: fake.email(), "status": "active"}\n'
2666
2929
  ' """\n'
2930
+ " # tina4:edit add seed data here\n"
2667
2931
  "__BODY__"
2668
2932
  "\n\n"
2669
2933
  "def run(db):\n"
@@ -2730,6 +2994,7 @@ def _gen_websocket(name: str, flags: dict = None):
2730
2994
  ' if event == "close":\n'
2731
2995
  " return\n"
2732
2996
  ' # event == "message"\n'
2997
+ " # tina4:edit handle inbound messages here\n"
2733
2998
  "__BODY__"
2734
2999
  )
2735
3000
  content = (
@@ -2781,6 +3046,7 @@ def _gen_listener(name: str, flags: dict = None):
2781
3046
  '@on("__EVENT__")\n'
2782
3047
  "def on___SLUG__(data=None):\n"
2783
3048
  " \"\"\"React to '__EVENT__'. `data` is whatever emit('__EVENT__', data) passed.\"\"\"\n"
3049
+ " # tina4:edit react to the event here\n"
2784
3050
  "__BODY__"
2785
3051
  )
2786
3052
  content = (
@@ -2866,6 +3132,7 @@ def _gen_form(name: str, flags: dict = None):
2866
3132
  ' <h1>{%% if item.id %%}Edit %s{%% else %%}Create %s{%% endif %%}</h1>\n'
2867
3133
  ' <form method="post" action="/api/%s{%% if item.id %%}/{{ item.id }}{%% endif %%}">\n'
2868
3134
  ' {{ form_token() }}\n'
3135
+ ' {# tina4:edit customise the form fields here #}\n'
2869
3136
  '%s'
2870
3137
  ' <button type="submit" class="btn btn-primary">\n'
2871
3138
  ' {%% if item.id %%}Update{%% else %%}Create{%% endif %%}\n'
@@ -2911,6 +3178,7 @@ def _gen_view(name: str, flags: dict = None):
2911
3178
  ' <h1>%s</h1>\n'
2912
3179
  ' <a href="/%s/create" class="btn btn-primary">Add %s</a>\n'
2913
3180
  ' </div>\n'
3181
+ ' {# tina4:edit customise the list columns or add filters here #}\n'
2914
3182
  ' <table class="table">\n'
2915
3183
  ' <thead>\n'
2916
3184
  ' <tr>\n'
@@ -2959,6 +3227,7 @@ def _gen_view(name: str, flags: dict = None):
2959
3227
  ' <a href="/%s" class="btn btn-outline-secondary">Back</a>\n'
2960
3228
  ' </div>\n'
2961
3229
  ' </div>\n'
3230
+ ' {# tina4:edit arrange the detail fields here #}\n'
2962
3231
  '%s\n'
2963
3232
  '</div>\n'
2964
3233
  '{%% endblock %%}\n'
@@ -3009,6 +3278,7 @@ def _gen_auth(name: str = None, flags: dict = None):
3009
3278
  ' user = User.create({\n'
3010
3279
  ' "email": email,\n'
3011
3280
  ' "password": Auth.hash_password(password),\n'
3281
+ ' # tina4:edit add roles/permissions here\n'
3012
3282
  ' "role": "user",\n'
3013
3283
  ' })\n'
3014
3284
  ' return response({"message": "Registered", "id": user.id}, 201)\n\n\n'
@@ -3375,12 +3645,14 @@ def _commands_manifest() -> dict:
3375
3645
  "framework": "python",
3376
3646
  "version": __version__,
3377
3647
  "commands": commands,
3378
- # ADR-0062: agents discover the generate resolution envelope shape
3379
- # programmatically. A version bump here is a break; adding an
3380
- # optional envelope key is not.
3648
+ # ADR-0062 / ADR-0063: agents discover the generate resolution envelope
3649
+ # shape programmatically. A version bump here is a break; adding an
3650
+ # optional envelope key is not. 1.1 (envelope generate_v1_1) added
3651
+ # `edit_hints[]` and `next[]` on the resolution — every v1 key
3652
+ # preserved, so a v1 consumer keeps working unchanged.
3381
3653
  "resolution_contract": {
3382
3654
  "version": _RESOLUTION_ENVELOPE_VERSION,
3383
- "envelope": "generate_v1",
3655
+ "envelope": _RESOLUTION_ENVELOPE_NAME,
3384
3656
  },
3385
3657
  }
3386
3658