tina4-python 3.13.119__tar.gz → 3.13.120__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.120}/PKG-INFO +1 -1
  2. {tina4_python-3.13.119 → tina4_python-3.13.120}/pyproject.toml +1 -1
  3. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/cli/__init__.py +263 -22
  5. {tina4_python-3.13.119 → tina4_python-3.13.120}/.gitignore +0 -0
  6. {tina4_python-3.13.119 → tina4_python-3.13.120}/README.md +0 -0
  7. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/_import_helper.py +0 -0
  11. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/ai/__init__.py +0 -0
  12. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/ai/client.py +0 -0
  13. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/container/__init__.py +0 -0
  17. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/context/__init__.py +0 -0
  18. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/context/chunker.py +0 -0
  19. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/__init__.py +0 -0
  20. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/cache.py +0 -0
  21. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/constants.py +0 -0
  22. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/events.py +0 -0
  23. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/middleware.py +0 -0
  24. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/port_takeover.py +0 -0
  25. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/core/server.py +0 -0
  30. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/crud/__init__.py +0 -0
  31. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/__init__.py +0 -0
  32. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/adapter.py +0 -0
  33. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/connection.py +0 -0
  34. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/database_url.py +0 -0
  35. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/firebird.py +0 -0
  36. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/mongodb.py +0 -0
  37. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/mssql.py +0 -0
  38. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/mysql.py +0 -0
  39. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/odbc.py +0 -0
  40. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/postgres.py +0 -0
  41. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/sql_translator.py +0 -0
  42. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/database/sqlite.py +0 -0
  43. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/debug/__init__.py +0 -0
  44. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/debug/error_overlay.py +0 -0
  45. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/dev_admin/__init__.py +0 -0
  46. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/dev_admin/metrics.py +0 -0
  47. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/dev_admin/plan.py +0 -0
  48. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/dev_admin/project_index.py +0 -0
  49. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/docs.py +0 -0
  50. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/docstore/__init__.py +0 -0
  51. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/dotenv/__init__.py +0 -0
  52. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/env.py +0 -0
  53. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/frond/FROND.md +0 -0
  54. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/frond/__init__.py +0 -0
  55. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/frond/compiler.py +0 -0
  56. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/frond/engine.py +0 -0
  57. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/frond/parser.py +0 -0
  58. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/auth/meta.json +0 -0
  59. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  60. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/database/meta.json +0 -0
  61. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  62. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/error-overlay/meta.json +0 -0
  63. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  64. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/orm/meta.json +0 -0
  65. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  66. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  67. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/queue/meta.json +0 -0
  68. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  69. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/rest-api/meta.json +0 -0
  70. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  71. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/templates/meta.json +0 -0
  72. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  73. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  74. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/__init__.py +0 -0
  75. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/adapter.py +0 -0
  76. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/adapters/__init__.py +0 -0
  77. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/adapters/arango.py +0 -0
  78. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/adapters/bolt.py +0 -0
  79. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/adapters/ultipa.py +0 -0
  80. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graph/graph_url.py +0 -0
  81. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/graphql/__init__.py +0 -0
  82. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/i18n/__init__.py +0 -0
  83. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/mcp/__init__.py +0 -0
  84. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/mcp/protocol.py +0 -0
  85. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/mcp/tools.py +0 -0
  86. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/messenger/__init__.py +0 -0
  87. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/migration/__init__.py +0 -0
  88. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/migration/runner.py +0 -0
  89. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/mqtt/__init__.py +0 -0
  90. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/mqtt/message.py +0 -0
  91. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/orm/__init__.py +0 -0
  92. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/orm/fields.py +0 -0
  93. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/orm/model.py +0 -0
  94. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/orm/point.py +0 -0
  95. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/__feedback/widget.js +0 -0
  96. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/css/tina4.css +0 -0
  97. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/css/tina4.min.css +0 -0
  98. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/favicon.ico +0 -0
  99. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/images/logo.svg +0 -0
  100. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  101. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/js/frond.js +0 -0
  102. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/js/frond.min.js +0 -0
  103. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  104. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/js/tina4.min.js +0 -0
  105. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/js/tina4js.min.js +0 -0
  106. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/swagger/index.html +0 -0
  107. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  108. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/query_builder/__init__.py +0 -0
  109. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/__init__.py +0 -0
  110. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/amqp_url.py +0 -0
  111. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/job.py +0 -0
  112. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/kafka_backend.py +0 -0
  113. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/lite_backend.py +0 -0
  114. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/mongo_backend.py +0 -0
  115. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue/rabbitmq_backend.py +0 -0
  116. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue_backends/__init__.py +0 -0
  117. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue_backends/kafka_backend.py +0 -0
  118. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue_backends/mongo_backend.py +0 -0
  119. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  120. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/__init__.py +0 -0
  121. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/models/Attachment.py +0 -0
  122. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/models/Channel.py +0 -0
  123. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/models/ChannelMember.py +0 -0
  124. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/models/Message.py +0 -0
  125. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/models/Workspace.py +0 -0
  126. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/models/__init__.py +0 -0
  127. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/realtime/storage.py +0 -0
  128. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/session_handlers/memcached_handler.py +0 -0
  133. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  134. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/session_handlers/redis_handler.py +0 -0
  135. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/session_handlers/valkey_handler.py +0 -0
  136. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/sso.py +0 -0
  137. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/swagger/__init__.py +0 -0
  138. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/components/crud.twig +0 -0
  139. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  140. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  141. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/docker/python/Dockerfile +0 -0
  142. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  143. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/302.twig +0 -0
  144. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/401.twig +0 -0
  145. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/403.twig +0 -0
  146. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/404.twig +0 -0
  147. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/500.twig +0 -0
  148. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/502.twig +0 -0
  149. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/503.twig +0 -0
  150. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/errors/base.twig +0 -0
  151. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/frontend/README.md +0 -0
  152. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/templates/readme.md +0 -0
  153. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/test/__init__.py +0 -0
  154. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/test_client/__init__.py +0 -0
  155. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  166. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  167. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/validator/__init__.py +0 -0
  168. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/websocket/__init__.py +0 -0
  169. {tina4_python-3.13.119 → tina4_python-3.13.120}/tina4_python/websocket/backplane.py +0 -0
  170. {tina4_python-3.13.119 → tina4_python-3.13.120}/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.120
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.120"
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.120"
55
55
 
56
56
 
57
57
  __version__ = _resolve_version()
@@ -986,7 +986,24 @@ _RESOLUTION_TARGETS = {"model", "route", "migration", "middleware"}
986
986
 
987
987
  # Envelope schema version. Bump when the SHAPE changes; adding an optional
988
988
  # key on the resolution object is not a break.
989
- _RESOLUTION_ENVELOPE_VERSION = "1"
989
+ #
990
+ # 1.1 (ADR-0063 Wave 1, 3.13.120): additive keys `edit_hints[]` and `next[]`
991
+ # on `resolution`, plus `test_paths[]` (already emitted since v1) is now
992
+ # surfaced in the human stderr block too. Every existing generator template
993
+ # bakes `tina4:edit` markers at first-edit spots so a caller sees exactly
994
+ # where to point a follow-up patch. Old envelope keys are preserved; a v1
995
+ # consumer sees the v1 shape intact and simply ignores the new keys.
996
+ _RESOLUTION_ENVELOPE_VERSION = "1.1"
997
+ _RESOLUTION_ENVELOPE_NAME = "generate_v1_1"
998
+
999
+ # Marker syntax the generator templates bake in at first-edit spots — accepts
1000
+ # a Python (`#`), SQL (`--`), or Twig (`{#`) comment prefix, followed by a
1001
+ # short actionable label. The label ends at end-of-line or a closing Twig `#}`.
1002
+ # NEVER matches inside a string literal — the compiler regex is a plain textual
1003
+ # scan of freshly-written source, but no template contains a matching literal.
1004
+ _EDIT_MARKER_RE = re.compile(
1005
+ r"(?:#|--|\{#)\s*tina4:edit\s+([^\n#]+?)\s*(?:#\})?\s*$"
1006
+ )
990
1007
 
991
1008
 
992
1009
  def _input_fields(flags: dict):
@@ -1002,6 +1019,118 @@ def _input_fields(flags: dict):
1002
1019
  return value
1003
1020
 
1004
1021
 
1022
+ def _scan_edit_hints(root: Path, rel_paths: list[str]) -> list[dict]:
1023
+ """Grep freshly-written files for `tina4:edit` markers (envelope v1.1).
1024
+
1025
+ Reads each `rel_paths` entry under `root`, walks its lines, and returns
1026
+ `[{file, line, label}]` — one entry per marker, in file+line order.
1027
+
1028
+ A missing file is silently skipped: the caller wires this into the
1029
+ envelope path where `actions_taken` is the source of truth for what
1030
+ landed. The scan is text-only — no imports, no parsing — so an edited
1031
+ template that fails to run still yields honest hints.
1032
+ """
1033
+ hints: list[dict] = []
1034
+ for rel in rel_paths:
1035
+ f = root / rel
1036
+ if not f.is_file():
1037
+ continue
1038
+ try:
1039
+ text = f.read_text(encoding="utf-8")
1040
+ except OSError:
1041
+ continue
1042
+ for lineno, line in enumerate(text.splitlines(), start=1):
1043
+ match = _EDIT_MARKER_RE.search(line)
1044
+ if match:
1045
+ hints.append({
1046
+ "file": rel,
1047
+ "line": lineno,
1048
+ "label": match.group(1).strip(),
1049
+ })
1050
+ return hints
1051
+
1052
+
1053
+ def _hint_paths_from_resolution(target: str, resolution: dict) -> list[str]:
1054
+ """The set of paths the caller should scan for `tina4:edit` markers.
1055
+
1056
+ Every path the generator ACTUALLY writes is worth scanning — the model
1057
+ file, the matching migration + down, the middleware file, the co-emitted
1058
+ test — because a marker in any of them names a first-edit spot the
1059
+ developer will hit. Order is stable: primary file, then migration + down,
1060
+ then any test paths declared in the envelope. The hints themselves land
1061
+ in file+line order regardless.
1062
+ """
1063
+ paths: list[str] = []
1064
+ primary = resolution.get("file_path")
1065
+ if primary:
1066
+ paths.append(primary)
1067
+
1068
+ # Model + migration composite: the migration is written by _gen_migration
1069
+ # under the model's own generator, so its markers deserve to surface too.
1070
+ if target == "model" and resolution.get("migration_path"):
1071
+ migration = resolution["migration_path"]
1072
+ paths.append(migration)
1073
+ if migration.endswith(".sql"):
1074
+ paths.append(migration[:-len(".sql")] + ".down.sql")
1075
+
1076
+ # A bare migration also writes its own .down.sql.
1077
+ if target == "migration" and primary and primary.endswith(".sql"):
1078
+ paths.append(primary[:-len(".sql")] + ".down.sql")
1079
+
1080
+ # Co-emitted tests carry their own markers (see the test template).
1081
+ for test_path in resolution.get("test_paths", []):
1082
+ if test_path not in paths:
1083
+ paths.append(test_path)
1084
+ return paths
1085
+
1086
+
1087
+ def _next_steps_for(target: str, name: str, resolution: dict, flags: dict) -> list[str]:
1088
+ """Curated actionable next-steps a caller should see after generation.
1089
+
1090
+ Kept short and concrete: what to edit, what to run, how to try it. The
1091
+ entries are per-target because a generic "next steps" list dilutes into
1092
+ noise — a route's next step is `curl`, a middleware's next step is
1093
+ `@middleware(...)` on a real route.
1094
+
1095
+ Placeholders are resolved from the same shape the envelope already
1096
+ reports (class_name, table_name, routes), so a caller can trust the
1097
+ strings match what actually landed on disk.
1098
+ """
1099
+ if target == "model":
1100
+ table = resolution.get("table_name") or _to_table(name)
1101
+ return [
1102
+ f"Edit src/orm/{name}.py to add fields beyond the default `name`",
1103
+ "Apply the migration: tina4 migrate",
1104
+ f"Scaffold CRUD around it: tina4 generate crud {name} --skip-model",
1105
+ f"Try it: tina4 serve -> curl http://localhost:7146/api/{table}",
1106
+ ]
1107
+
1108
+ if target == "route":
1109
+ route_path = name.lstrip("/")
1110
+ first_url = (resolution.get("routes") or [f"/api/{route_path}"])[0]
1111
+ return [
1112
+ f"Edit src/routes/{route_path}.py to customise the handlers",
1113
+ "Start the server: tina4 serve",
1114
+ f"Try it: curl http://localhost:7146{first_url}",
1115
+ ]
1116
+
1117
+ if target == "migration":
1118
+ return [
1119
+ "Apply it: tina4 migrate",
1120
+ "Roll back last batch: tina4 migrate --rollback",
1121
+ "Inspect the pending: tina4 migrate --status",
1122
+ ]
1123
+
1124
+ if target == "middleware":
1125
+ snake = _to_snake(name)
1126
+ return [
1127
+ f"Register it on a route with @middleware({name}) in src/routes/",
1128
+ f"Run the co-emitted test: .venv/bin/python -m pytest tests/test_{snake}.py",
1129
+ ]
1130
+
1131
+ return []
1132
+
1133
+
1005
1134
  def _resolve_generation(target: str, name: str, flags: dict,
1006
1135
  timestamp: str | None = None) -> dict:
1007
1136
  """Compute the resolution mapping for a generate call. Pure — no writes.
@@ -1112,12 +1241,19 @@ def _actions_from_resolution(target: str, resolution: dict) -> list[str]:
1112
1241
 
1113
1242
 
1114
1243
  def _format_resolution_block(name: str, target: str, resolution: dict,
1115
- dry_run: bool = False) -> str:
1244
+ dry_run: bool = False,
1245
+ edit_hints: list[dict] | None = None,
1246
+ next_steps: list[str] | None = None) -> str:
1116
1247
  """Render the human-readable resolution block for stderr on bare calls.
1117
1248
 
1118
1249
  Same shape as the plan / ADR-0062 example. A generator with no route/table
1119
1250
  footprint (middleware, route without a matching table) simply omits the
1120
1251
  lines that don't apply — the block is honest about what was chosen.
1252
+
1253
+ ADR-0063 (envelope v1.1) also surfaces `test_paths[]` (was in the envelope
1254
+ since v1, never printed), plus `edit_hints[]` under "Edit these lines:"
1255
+ and `next_steps[]` under "Next:". Each section omits when empty so the
1256
+ block never grows a header without content beneath it.
1121
1257
  """
1122
1258
  suffix = " (dry-run — nothing written)" if dry_run else ""
1123
1259
  lines = [f"Generated {target} {name}{suffix}"]
@@ -1145,6 +1281,13 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
1145
1281
  if target != "migration" and resolution.get("migration_path"):
1146
1282
  lines.append(f" migration {resolution['migration_path']}")
1147
1283
 
1284
+ # ADR-0063: surface test_paths[] in the human block too (it has been in the
1285
+ # envelope since v1 but only the JSON path printed it). Skipped when empty
1286
+ # so a migration without a test file doesn't grow an empty "tests" row.
1287
+ test_paths = resolution.get("test_paths") or []
1288
+ if test_paths:
1289
+ lines.append(f" tests {', '.join(test_paths)}")
1290
+
1148
1291
  if pluralize:
1149
1292
  lines.append("")
1150
1293
  lines.append(f" To keep the raw name '{pluralize['from']}' as the table:")
@@ -1154,21 +1297,47 @@ def _format_resolution_block(name: str, target: str, resolution: dict,
1154
1297
  "(opt-in, ADR-0062 forthcoming)"
1155
1298
  )
1156
1299
 
1300
+ # ADR-0063 additive sections. Each renders only when there is content, so
1301
+ # a target with no markers or no next-steps stays as tight as before.
1302
+ if edit_hints:
1303
+ lines.append("")
1304
+ lines.append("Edit these lines:")
1305
+ # Left-pad the file:line column so labels align — makes scanning easier
1306
+ # when there are three or four hints of different path lengths.
1307
+ addrs = [f"{h['file']}:{h['line']}" for h in edit_hints]
1308
+ width = max(len(a) for a in addrs)
1309
+ for addr, hint in zip(addrs, edit_hints):
1310
+ lines.append(f" {addr:<{width}} {hint['label']}")
1311
+
1312
+ if next_steps:
1313
+ lines.append("")
1314
+ lines.append("Next:")
1315
+ for i, step in enumerate(next_steps, start=1):
1316
+ lines.append(f" {i}. {step}")
1317
+
1157
1318
  return "\n".join(lines) + "\n"
1158
1319
 
1159
1320
 
1160
1321
  def _emit_resolution(target: str, name: str, flags: dict) -> None:
1161
1322
  """Handle the --json / --dry-run / bare paths for the 4 wired generators.
1162
1323
 
1163
- Contract shared by every language (ADR-0062):
1324
+ Contract shared by every language (ADR-0062, extended by ADR-0063):
1164
1325
  - --json → envelope on stdout; nothing else on stdout.
1165
1326
  - --dry-run → skip every write; envelope with dry_run=true.
1166
1327
  - --json + --dry-run → both compose (envelope with actions_taken=[]).
1167
1328
  - bare → normal write, then human resolution block on stderr.
1329
+
1330
+ Envelope v1.1 (ADR-0063) additively surfaces `resolution.edit_hints[]`
1331
+ (one per `tina4:edit` marker found in freshly-written files) and
1332
+ `resolution.next[]` (curated actionable next-steps per verb). The
1333
+ hints are scanned from disk on the write paths; on --dry-run the
1334
+ generator runs in a throw-away tmpdir so the same scan gives the
1335
+ caller a real preview without touching the working tree.
1168
1336
  """
1169
1337
  import contextlib
1170
1338
  import io
1171
1339
  import json as _json
1340
+ import tempfile
1172
1341
 
1173
1342
  dry_run = bool(flags.get("dry-run"))
1174
1343
  want_json = bool(flags.get("json"))
@@ -1178,21 +1347,55 @@ def _emit_resolution(target: str, name: str, flags: dict) -> None:
1178
1347
  timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
1179
1348
  resolution = _resolve_generation(target, name, flags, timestamp=timestamp)
1180
1349
 
1350
+ handler = GENERATORS[target]["handler"]
1351
+ generator_flags = dict(flags)
1352
+ generator_flags["_timestamp"] = timestamp
1353
+ # `_gen_*` handlers read the primary flags; `--dry-run` / `--json` are
1354
+ # envelope-only concerns, they must not leak into the underlying writer.
1355
+ generator_flags.pop("dry-run", None)
1356
+ generator_flags.pop("json", None)
1357
+
1181
1358
  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
1359
+ edit_hints: list[dict] = []
1360
+
1361
+ hint_paths = _hint_paths_from_resolution(target, resolution)
1362
+
1363
+ if dry_run:
1364
+ # Render into a throw-away tree so the scan sees the SAME bytes the
1365
+ # writer would drop on disk. The envelope's paths still point at the
1366
+ # real project paths (unchanged by the tmpdir chdir) — the caller
1367
+ # can trust `edit_hints[].file` and `resolution.file_path` share a
1368
+ # coordinate space.
1369
+ with tempfile.TemporaryDirectory() as tmp:
1370
+ original_cwd = os.getcwd()
1371
+ os.chdir(tmp)
1372
+ try:
1373
+ # Suppress the generator's own "Created X" lines — this is a
1374
+ # dry-run, and its "creations" happen in a tmpdir the caller
1375
+ # will never see. `--json` also swallows stdout on the real
1376
+ # write path; this keeps the two paths symmetrical.
1377
+ with contextlib.redirect_stdout(io.StringIO()):
1378
+ handler(name, generator_flags)
1379
+ edit_hints = _scan_edit_hints(Path(tmp), hint_paths)
1380
+ finally:
1381
+ os.chdir(original_cwd)
1382
+ else:
1383
+ # Real write. When --json we swallow the generator's stdout so the
1185
1384
  # JSON envelope is the SOLE thing on stdout; otherwise we let its
1186
1385
  # "Created X" lines through as before.
1187
- generator_flags = dict(flags)
1188
- generator_flags["_timestamp"] = timestamp
1189
- handler = GENERATORS[target]["handler"]
1190
-
1191
1386
  stdout_sink = io.StringIO() if want_json else sys.stdout
1192
1387
  with contextlib.redirect_stdout(stdout_sink):
1193
1388
  handler(name, generator_flags)
1194
1389
 
1195
1390
  actions_taken = _actions_from_resolution(target, resolution)
1391
+ edit_hints = _scan_edit_hints(Path.cwd(), hint_paths)
1392
+
1393
+ # ADR-0063 additive envelope keys — computed once and reused by the JSON
1394
+ # and human paths so what the caller sees on stderr matches the envelope
1395
+ # byte-for-byte.
1396
+ next_steps = _next_steps_for(target, name, resolution, flags)
1397
+ resolution["edit_hints"] = edit_hints
1398
+ resolution["next"] = next_steps
1196
1399
 
1197
1400
  envelope = {
1198
1401
  "command": "generate",
@@ -1209,8 +1412,12 @@ def _emit_resolution(target: str, name: str, flags: dict) -> None:
1209
1412
  # Human resolution block always lands on STDERR (per plan): stdout
1210
1413
  # keeps whatever the generator printed on the bare path, and the
1211
1414
  # block never contends with a caller parsing stdout.
1212
- sys.stderr.write(_format_resolution_block(name, target, resolution,
1213
- dry_run=dry_run))
1415
+ sys.stderr.write(_format_resolution_block(
1416
+ name, target, resolution,
1417
+ dry_run=dry_run,
1418
+ edit_hints=edit_hints,
1419
+ next_steps=next_steps,
1420
+ ))
1214
1421
 
1215
1422
 
1216
1423
  # ── Generate (rich scaffolding) ───────────────────────────────────────
@@ -1294,11 +1501,18 @@ def _gen_model(name: str, flags: dict, *, emit_test: bool = True):
1294
1501
 
1295
1502
  imports = ", ".join(sorted(used_types))
1296
1503
 
1297
- # Build field lines
1298
- field_lines = [f" id = IntegerField(primary_key=True, auto_increment=True)"]
1504
+ # Build field lines. Bracketed by two `tina4:edit` markers (ADR-0063) so
1505
+ # a first-time user sees WHERE to add columns and relationships — the
1506
+ # `id` and `created_at` lines are framework-managed, the block between
1507
+ # is theirs to grow.
1508
+ field_lines = [
1509
+ " id = IntegerField(primary_key=True, auto_increment=True)",
1510
+ " # tina4:edit add fields here",
1511
+ ]
1299
1512
  for fname, ftype in fields:
1300
1513
  info = FIELD_TYPE_MAP.get(ftype, FIELD_TYPE_MAP["string"])
1301
1514
  field_lines.append(f" {fname} = {info['orm']}()")
1515
+ field_lines.append(" # tina4:edit add relationships here (e.g. author = ForeignKeyField(to=Author))")
1302
1516
  field_lines.append(" created_at = DateTimeField()")
1303
1517
 
1304
1518
  # Write model file
@@ -1395,6 +1609,7 @@ async def list_{route_path}(request, response):
1395
1609
  per_page = int(request.query.get("per_page", 20))
1396
1610
  offset = (page - 1) * per_page
1397
1611
  records, total = {model}.where("1=1", limit=per_page, offset=offset, with_count=True)
1612
+ # tina4:edit customise the list projection (filters, ordering, fields)
1398
1613
  return response({{
1399
1614
  "records": [r.to_dict() for r in records],
1400
1615
  "count": total,
@@ -1588,6 +1803,7 @@ def _gen_crud(name: str, flags: dict):
1588
1803
  '{% block content %}\n'
1589
1804
  '<div class="container mt-4">\n'
1590
1805
  f' <h1>{name}s</h1>\n'
1806
+ ' {# tina4:edit restrict fields exposed to the API here #}\n'
1591
1807
  ' <table class="table">\n'
1592
1808
  ' <thead>\n'
1593
1809
  ' <tr>\n'
@@ -1671,11 +1887,21 @@ def _gen_migration(name: str, flags: dict = None, *,
1671
1887
  col_lines.append(f" {fname} {info['sql']}{default}")
1672
1888
  col_lines.append(" created_at TEXT DEFAULT CURRENT_TIMESTAMP")
1673
1889
 
1674
- up_sql = f"CREATE TABLE IF NOT EXISTS {table} (\n" + ",\n".join(col_lines) + "\n);"
1890
+ # ADR-0063: `tina4:edit` marker between the last column and the closing
1891
+ # paren, placed OUTSIDE the comma-joined column list so the SQL stays
1892
+ # valid. Points a first-time user at where to add columns beyond the
1893
+ # framework-managed id + created_at.
1894
+ up_sql = (
1895
+ f"CREATE TABLE IF NOT EXISTS {table} (\n"
1896
+ + ",\n".join(col_lines)
1897
+ + "\n -- tina4:edit add columns beyond id + created_at\n);"
1898
+ )
1675
1899
  down_sql = f"DROP TABLE IF EXISTS {table};"
1676
1900
  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;"
1901
+ up_sql = (f"-- tina4:edit write the UP migration SQL\n"
1902
+ f"-- Example: ALTER TABLE {table} ADD COLUMN new_col TEXT DEFAULT '';")
1903
+ down_sql = (f"-- tina4:edit write the DOWN rollback SQL\n"
1904
+ f"-- Example: ALTER TABLE {table} DROP COLUMN new_col;")
1679
1905
 
1680
1906
  content = (
1681
1907
  f"-- Migration: {name}\n"
@@ -1739,12 +1965,14 @@ class {name}:
1739
1965
  Return (request, response) to continue, or
1740
1966
  return (request, response("error", 401)) to block.
1741
1967
  """
1968
+ # tina4:edit guard the request here (auth check, rate limit, headers)
1742
1969
  Log.info(f"{name}: {{request.method}} {{request.url}}")
1743
1970
  return request, response
1744
1971
 
1745
1972
  @staticmethod
1746
1973
  def after_{snake}(request, response):
1747
1974
  """Runs after the route handler."""
1975
+ # tina4:edit inject headers or audit the response here
1748
1976
  return request, response
1749
1977
  '''
1750
1978
  path.write_text(content, encoding="utf-8")
@@ -1861,7 +2089,7 @@ class Test{model}:
1861
2089
 
1862
2090
  def test_list_{snake}(self):
1863
2091
  """Test listing {snake}."""
1864
- # TODO: implement
2092
+ # tina4:edit add assertions here (real DB, real request — no mocks)
1865
2093
  assert True
1866
2094
 
1867
2095
  def test_get_{singular}(self):
@@ -1902,6 +2130,7 @@ class Test{name.title().replace("_", "")}:
1902
2130
 
1903
2131
  def test_example(self):
1904
2132
  """Example test — replace with real tests."""
2133
+ # tina4:edit add assertions here (real DB, real request — no mocks)
1905
2134
  assert True
1906
2135
  '''
1907
2136
 
@@ -2463,6 +2692,7 @@ def _gen_service(name: str, flags: dict = None):
2463
2692
  ' """The scheduled task body. `context` is a ServiceContext with\n'
2464
2693
  " .name, .stop_event and .log; a daemon-style task would loop until\n"
2465
2694
  ' context.stop_event.is_set()."""\n'
2695
+ " # tina4:edit main service loop here\n"
2466
2696
  "__BODY__"
2467
2697
  "\n\n"
2468
2698
  "def register(runner: ServiceRunner) -> None:\n"
@@ -2534,6 +2764,7 @@ def _gen_queue(name: str, flags: dict = None):
2534
2764
  "\n"
2535
2765
  "def handle___SLUG__(data: dict):\n"
2536
2766
  ' """Process ONE __TOPIC__ job payload (`data` is Job.data — the pushed dict)."""\n'
2767
+ " # tina4:edit handle the job payload here\n"
2537
2768
  "__BODY__"
2538
2769
  "\n\n"
2539
2770
  "def consume___SLUG__(context=None):\n"
@@ -2612,6 +2843,7 @@ def _gen_validator(name: str, flags: dict = None):
2612
2843
  ' return response.error("VALIDATION_FAILED", v.errors()[0]["message"], 400)\n'
2613
2844
  ' """\n'
2614
2845
  " validator = Validator(data)\n"
2846
+ " # tina4:edit add validation rules here\n"
2615
2847
  "__BODY__"
2616
2848
  " return validator\n"
2617
2849
  )
@@ -2664,6 +2896,7 @@ def _gen_seeder(name: str, flags: dict = None):
2664
2896
  " need a specific shape here. Each callable receives a FakeData instance:\n"
2665
2897
  ' return {"email": lambda fake: fake.email(), "status": "active"}\n'
2666
2898
  ' """\n'
2899
+ " # tina4:edit add seed data here\n"
2667
2900
  "__BODY__"
2668
2901
  "\n\n"
2669
2902
  "def run(db):\n"
@@ -2730,6 +2963,7 @@ def _gen_websocket(name: str, flags: dict = None):
2730
2963
  ' if event == "close":\n'
2731
2964
  " return\n"
2732
2965
  ' # event == "message"\n'
2966
+ " # tina4:edit handle inbound messages here\n"
2733
2967
  "__BODY__"
2734
2968
  )
2735
2969
  content = (
@@ -2781,6 +3015,7 @@ def _gen_listener(name: str, flags: dict = None):
2781
3015
  '@on("__EVENT__")\n'
2782
3016
  "def on___SLUG__(data=None):\n"
2783
3017
  " \"\"\"React to '__EVENT__'. `data` is whatever emit('__EVENT__', data) passed.\"\"\"\n"
3018
+ " # tina4:edit react to the event here\n"
2784
3019
  "__BODY__"
2785
3020
  )
2786
3021
  content = (
@@ -2866,6 +3101,7 @@ def _gen_form(name: str, flags: dict = None):
2866
3101
  ' <h1>{%% if item.id %%}Edit %s{%% else %%}Create %s{%% endif %%}</h1>\n'
2867
3102
  ' <form method="post" action="/api/%s{%% if item.id %%}/{{ item.id }}{%% endif %%}">\n'
2868
3103
  ' {{ form_token() }}\n'
3104
+ ' {# tina4:edit customise the form fields here #}\n'
2869
3105
  '%s'
2870
3106
  ' <button type="submit" class="btn btn-primary">\n'
2871
3107
  ' {%% if item.id %%}Update{%% else %%}Create{%% endif %%}\n'
@@ -2911,6 +3147,7 @@ def _gen_view(name: str, flags: dict = None):
2911
3147
  ' <h1>%s</h1>\n'
2912
3148
  ' <a href="/%s/create" class="btn btn-primary">Add %s</a>\n'
2913
3149
  ' </div>\n'
3150
+ ' {# tina4:edit customise the list columns or add filters here #}\n'
2914
3151
  ' <table class="table">\n'
2915
3152
  ' <thead>\n'
2916
3153
  ' <tr>\n'
@@ -2959,6 +3196,7 @@ def _gen_view(name: str, flags: dict = None):
2959
3196
  ' <a href="/%s" class="btn btn-outline-secondary">Back</a>\n'
2960
3197
  ' </div>\n'
2961
3198
  ' </div>\n'
3199
+ ' {# tina4:edit arrange the detail fields here #}\n'
2962
3200
  '%s\n'
2963
3201
  '</div>\n'
2964
3202
  '{%% endblock %%}\n'
@@ -3009,6 +3247,7 @@ def _gen_auth(name: str = None, flags: dict = None):
3009
3247
  ' user = User.create({\n'
3010
3248
  ' "email": email,\n'
3011
3249
  ' "password": Auth.hash_password(password),\n'
3250
+ ' # tina4:edit add roles/permissions here\n'
3012
3251
  ' "role": "user",\n'
3013
3252
  ' })\n'
3014
3253
  ' return response({"message": "Registered", "id": user.id}, 201)\n\n\n'
@@ -3375,12 +3614,14 @@ def _commands_manifest() -> dict:
3375
3614
  "framework": "python",
3376
3615
  "version": __version__,
3377
3616
  "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.
3617
+ # ADR-0062 / ADR-0063: agents discover the generate resolution envelope
3618
+ # shape programmatically. A version bump here is a break; adding an
3619
+ # optional envelope key is not. 1.1 (envelope generate_v1_1) added
3620
+ # `edit_hints[]` and `next[]` on the resolution — every v1 key
3621
+ # preserved, so a v1 consumer keeps working unchanged.
3381
3622
  "resolution_contract": {
3382
3623
  "version": _RESOLUTION_ENVELOPE_VERSION,
3383
- "envelope": "generate_v1",
3624
+ "envelope": _RESOLUTION_ENVELOPE_NAME,
3384
3625
  },
3385
3626
  }
3386
3627