gina 0.6.3 → 0.6.4

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 (567) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +32 -40
  3. package/ROADMAP.md +3 -1
  4. package/framework/v0.6.4/VERSION +1 -0
  5. package/framework/v0.6.4/core/asset/plugin/dist/vendor/gina/css/gina.min.css +1 -0
  6. package/framework/v0.6.4/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
  7. package/framework/v0.6.4/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
  8. package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/js/gina.js +1522 -79
  9. package/framework/v0.6.4/core/asset/plugin/dist/vendor/gina/js/gina.min.js +640 -0
  10. package/framework/v0.6.4/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  11. package/framework/v0.6.4/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  12. package/framework/{v0.6.3 → v0.6.4}/core/config.js +22 -4
  13. package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.js +62 -2
  14. package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-nunjucks.js +11 -3
  15. package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-swig.js +15 -1
  16. package/framework/{v0.6.3 → v0.6.4}/core/gna.js +33 -1
  17. package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/validator/src/main.js +892 -21
  18. package/framework/{v0.6.3 → v0.6.4}/core/server.js +60 -2
  19. package/framework/v0.6.4/core/template/_gitignore +27 -0
  20. package/framework/{v0.6.3 → v0.6.4}/lib/cmd/secrets/arguments.json +1 -0
  21. package/framework/v0.6.4/lib/cmd/secrets/check.js +861 -0
  22. package/framework/{v0.6.3 → v0.6.4}/lib/cmd/secrets/help.txt +29 -5
  23. package/framework/v0.6.4/lib/loading-state/package.json +7 -0
  24. package/framework/v0.6.4/lib/loading-state/src/main.js +366 -0
  25. package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/mq/listener.js +23 -0
  26. package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/mq/speaker.js +27 -0
  27. package/framework/v0.6.4/lib/secrets/src/backends/file.js +180 -0
  28. package/framework/v0.6.4/lib/secrets/src/env-file.js +220 -0
  29. package/framework/{v0.6.3 → v0.6.4}/lib/secrets/src/main.js +84 -1
  30. package/framework/{v0.6.3 → v0.6.4}/package.json +1 -1
  31. package/gna.js +4 -4
  32. package/llms.txt +7 -7
  33. package/package.json +3 -3
  34. package/schema/settings.json +14 -0
  35. package/framework/v0.6.3/VERSION +0 -1
  36. package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -1
  37. package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
  38. package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
  39. package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js +0 -625
  40. package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  41. package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  42. package/framework/v0.6.3/core/template/_gitignore +0 -19
  43. package/framework/v0.6.3/lib/cmd/secrets/check.js +0 -507
  44. /package/framework/{v0.6.3 → v0.6.4}/AUTHORS +0 -0
  45. /package/framework/{v0.6.3 → v0.6.4}/LICENSE +0 -0
  46. /package/framework/{v0.6.3 → v0.6.4}/core/asset/html/nolayout.html +0 -0
  47. /package/framework/{v0.6.3 → v0.6.4}/core/asset/html/static.html +0 -0
  48. /package/framework/{v0.6.3 → v0.6.4}/core/asset/img/android-chrome-192x192.png +0 -0
  49. /package/framework/{v0.6.3 → v0.6.4}/core/asset/img/android-chrome-512x512.png +0 -0
  50. /package/framework/{v0.6.3 → v0.6.4}/core/asset/img/apple-touch-icon.png +0 -0
  51. /package/framework/{v0.6.3 → v0.6.4}/core/asset/img/favicon-16x16.png +0 -0
  52. /package/framework/{v0.6.3 → v0.6.4}/core/asset/img/favicon-32x32.png +0 -0
  53. /package/framework/{v0.6.3 → v0.6.4}/core/asset/img/favicon.ico +0 -0
  54. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/README.md +0 -0
  55. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
  56. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
  57. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
  58. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +0 -0
  59. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
  60. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
  61. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
  62. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/inspector/index.html +0 -0
  63. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +0 -0
  64. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +0 -0
  65. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
  66. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
  67. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
  68. /package/framework/{v0.6.3 → v0.6.4}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
  69. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/ai/index.js +0 -0
  70. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/ai/lib/connector.js +0 -0
  71. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/index.js +0 -0
  72. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/connector.js +0 -0
  73. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/connector.v3.js +0 -0
  74. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/connector.v4.js +0 -0
  75. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/n1ql.js +0 -0
  76. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/session-store.js +0 -0
  77. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/session-store.v3.js +0 -0
  78. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/couchbase/lib/session-store.v4.js +0 -0
  79. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/duckdb/index.js +0 -0
  80. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/duckdb/lib/connector.js +0 -0
  81. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mongodb/index.js +0 -0
  82. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mongodb/lib/connector.js +0 -0
  83. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mongodb/lib/job-store.js +0 -0
  84. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
  85. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mongodb/lib/session-store.js +0 -0
  86. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mysql/index.js +0 -0
  87. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/mysql/lib/connector.js +0 -0
  88. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/postgresql/index.js +0 -0
  89. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/postgresql/lib/connector.js +0 -0
  90. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/redis/index.js +0 -0
  91. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/redis/lib/connector.js +0 -0
  92. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/redis/lib/job-store.js +0 -0
  93. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/redis/lib/render-cache-store.js +0 -0
  94. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/redis/lib/session-store.js +0 -0
  95. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/scylladb/index.js +0 -0
  96. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/scylladb/lib/connector.js +0 -0
  97. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/scylladb/lib/session-store.js +0 -0
  98. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/sql-parser.js +0 -0
  99. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/sqlite/index.js +0 -0
  100. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/sqlite/lib/connector.js +0 -0
  101. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/sqlite/lib/job-store.js +0 -0
  102. /package/framework/{v0.6.3 → v0.6.4}/core/connectors/sqlite/lib/session-store.js +0 -0
  103. /package/framework/{v0.6.3 → v0.6.4}/core/content.encoding +0 -0
  104. /package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.framework.js +0 -0
  105. /package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-json.js +0 -0
  106. /package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-nunjucks-async.js +0 -0
  107. /package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-stream.js +0 -0
  108. /package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-swig-async.js +0 -0
  109. /package/framework/{v0.6.3 → v0.6.4}/core/controller/controller.render-v1.js +0 -0
  110. /package/framework/{v0.6.3 → v0.6.4}/core/controller/index.js +0 -0
  111. /package/framework/{v0.6.3 → v0.6.4}/core/controller/inspector-window-emit.js +0 -0
  112. /package/framework/{v0.6.3 → v0.6.4}/core/controller/release-banner.js +0 -0
  113. /package/framework/{v0.6.3 → v0.6.4}/core/dev/index.js +0 -0
  114. /package/framework/{v0.6.3 → v0.6.4}/core/dev/lib/class.js +0 -0
  115. /package/framework/{v0.6.3 → v0.6.4}/core/dev/lib/factory.js +0 -0
  116. /package/framework/{v0.6.3 → v0.6.4}/core/dev/lib/tools.js +0 -0
  117. /package/framework/{v0.6.3 → v0.6.4}/core/locales/README.md +0 -0
  118. /package/framework/{v0.6.3 → v0.6.4}/core/locales/currency.json +0 -0
  119. /package/framework/{v0.6.3 → v0.6.4}/core/locales/dist/language/en.json +0 -0
  120. /package/framework/{v0.6.3 → v0.6.4}/core/locales/dist/language/fr.json +0 -0
  121. /package/framework/{v0.6.3 → v0.6.4}/core/locales/dist/region/en.json +0 -0
  122. /package/framework/{v0.6.3 → v0.6.4}/core/locales/dist/region/fr.json +0 -0
  123. /package/framework/{v0.6.3 → v0.6.4}/core/locales/index.js +0 -0
  124. /package/framework/{v0.6.3 → v0.6.4}/core/mime.types +0 -0
  125. /package/framework/{v0.6.3 → v0.6.4}/core/model/entity.js +0 -0
  126. /package/framework/{v0.6.3 → v0.6.4}/core/model/index.js +0 -0
  127. /package/framework/{v0.6.3 → v0.6.4}/core/model/template/entityFactory.js +0 -0
  128. /package/framework/{v0.6.3 → v0.6.4}/core/model/template/index.js +0 -0
  129. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/README.md +0 -0
  130. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/index.js +0 -0
  131. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/csrf/README.md +0 -0
  132. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/csrf/package.json +0 -0
  133. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/csrf/src/main.js +0 -0
  134. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/README.md +0 -0
  135. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/coep/README.md +0 -0
  136. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/coep/package.json +0 -0
  137. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
  138. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/coop/README.md +0 -0
  139. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/coop/package.json +0 -0
  140. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
  141. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/corp/README.md +0 -0
  142. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/corp/package.json +0 -0
  143. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
  144. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/csp/README.md +0 -0
  145. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/csp/package.json +0 -0
  146. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
  147. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/hide-powered-by/README.md +0 -0
  148. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
  149. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
  150. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/hsts/README.md +0 -0
  151. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/hsts/package.json +0 -0
  152. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
  153. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
  154. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
  155. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
  156. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/package.json +0 -0
  157. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
  158. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
  159. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
  160. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/src/main.js +0 -0
  161. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
  162. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
  163. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
  164. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
  165. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
  166. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
  167. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
  168. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
  169. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
  170. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
  171. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
  172. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
  173. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
  174. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
  175. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
  176. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
  177. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
  178. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
  179. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/session/README.md +0 -0
  180. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/session/package.json +0 -0
  181. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/session/src/main.js +0 -0
  182. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/storage/README.md +0 -0
  183. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/storage/build.json +0 -0
  184. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/storage/package.json +0 -0
  185. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/storage/src/main.js +0 -0
  186. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/validator/README.md +0 -0
  187. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/validator/build.json +0 -0
  188. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/validator/package.json +0 -0
  189. /package/framework/{v0.6.3 → v0.6.4}/core/plugins/lib/validator/src/form-validator.js +0 -0
  190. /package/framework/{v0.6.3 → v0.6.4}/core/router.js +0 -0
  191. /package/framework/{v0.6.3 → v0.6.4}/core/server.express.js +0 -0
  192. /package/framework/{v0.6.3 → v0.6.4}/core/server.isaac.js +0 -0
  193. /package/framework/{v0.6.3 → v0.6.4}/core/status.codes +0 -0
  194. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/app.json +0 -0
  195. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/connectors.json +0 -0
  196. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/routing.json +0 -0
  197. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/settings.json +0 -0
  198. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
  199. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/templates.json +0 -0
  200. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/config/watchers.json +0 -0
  201. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
  202. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
  203. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
  204. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/index.js +0 -0
  205. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle/locales/en.json +0 -0
  206. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
  207. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/css/default.css +0 -0
  208. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/css/home.css +0 -0
  209. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
  210. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
  211. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
  212. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
  213. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
  214. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/readme.md +0 -0
  215. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_public/sw.js +0 -0
  216. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
  217. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
  218. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
  219. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
  220. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
  221. /package/framework/{v0.6.3 → v0.6.4}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
  222. /package/framework/{v0.6.3 → v0.6.4}/core/template/command/gina.bat.tpl +0 -0
  223. /package/framework/{v0.6.3 → v0.6.4}/core/template/command/gina.tpl +0 -0
  224. /package/framework/{v0.6.3 → v0.6.4}/core/template/conf/env.json +0 -0
  225. /package/framework/{v0.6.3 → v0.6.4}/core/template/conf/manifest.json +0 -0
  226. /package/framework/{v0.6.3 → v0.6.4}/core/template/conf/package.json +0 -0
  227. /package/framework/{v0.6.3 → v0.6.4}/core/template/conf/settings.json +0 -0
  228. /package/framework/{v0.6.3 → v0.6.4}/core/template/conf/statics.json +0 -0
  229. /package/framework/{v0.6.3 → v0.6.4}/core/template/conf/templates.json +0 -0
  230. /package/framework/{v0.6.3 → v0.6.4}/core/template/error/client/json/401.json +0 -0
  231. /package/framework/{v0.6.3 → v0.6.4}/core/template/error/client/json/403.json +0 -0
  232. /package/framework/{v0.6.3 → v0.6.4}/core/template/error/client/json/404.json +0 -0
  233. /package/framework/{v0.6.3 → v0.6.4}/core/template/error/server/html/50x.html +0 -0
  234. /package/framework/{v0.6.3 → v0.6.4}/core/template/error/server/json/500.json +0 -0
  235. /package/framework/{v0.6.3 → v0.6.4}/core/template/error/server/json/503.json +0 -0
  236. /package/framework/{v0.6.3 → v0.6.4}/core/template/extensions/logger/config.json +0 -0
  237. /package/framework/{v0.6.3 → v0.6.4}/helpers/console.js +0 -0
  238. /package/framework/{v0.6.3 → v0.6.4}/helpers/context.js +0 -0
  239. /package/framework/{v0.6.3 → v0.6.4}/helpers/data/LICENSE +0 -0
  240. /package/framework/{v0.6.3 → v0.6.4}/helpers/data/README.md +0 -0
  241. /package/framework/{v0.6.3 → v0.6.4}/helpers/data/package.json +0 -0
  242. /package/framework/{v0.6.3 → v0.6.4}/helpers/data/src/main.js +0 -0
  243. /package/framework/{v0.6.3 → v0.6.4}/helpers/dateFormat.js +0 -0
  244. /package/framework/{v0.6.3 → v0.6.4}/helpers/index.js +0 -0
  245. /package/framework/{v0.6.3 → v0.6.4}/helpers/json/LICENSE +0 -0
  246. /package/framework/{v0.6.3 → v0.6.4}/helpers/json/README.md +0 -0
  247. /package/framework/{v0.6.3 → v0.6.4}/helpers/json/package.json +0 -0
  248. /package/framework/{v0.6.3 → v0.6.4}/helpers/json/src/main.js +0 -0
  249. /package/framework/{v0.6.3 → v0.6.4}/helpers/path.js +0 -0
  250. /package/framework/{v0.6.3 → v0.6.4}/helpers/plugins/README.md +0 -0
  251. /package/framework/{v0.6.3 → v0.6.4}/helpers/plugins/package.json +0 -0
  252. /package/framework/{v0.6.3 → v0.6.4}/helpers/plugins/src/api-error.js +0 -0
  253. /package/framework/{v0.6.3 → v0.6.4}/helpers/plugins/src/main.js +0 -0
  254. /package/framework/{v0.6.3 → v0.6.4}/helpers/prototypes.js +0 -0
  255. /package/framework/{v0.6.3 → v0.6.4}/helpers/task.js +0 -0
  256. /package/framework/{v0.6.3 → v0.6.4}/helpers/text.js +0 -0
  257. /package/framework/{v0.6.3 → v0.6.4}/lib/admin/package.json +0 -0
  258. /package/framework/{v0.6.3 → v0.6.4}/lib/admin/src/main.js +0 -0
  259. /package/framework/{v0.6.3 → v0.6.4}/lib/archiver/README.md +0 -0
  260. /package/framework/{v0.6.3 → v0.6.4}/lib/archiver/build.json +0 -0
  261. /package/framework/{v0.6.3 → v0.6.4}/lib/archiver/package.json +0 -0
  262. /package/framework/{v0.6.3 → v0.6.4}/lib/archiver/src/dep/jszip.min.js +0 -0
  263. /package/framework/{v0.6.3 → v0.6.4}/lib/archiver/src/main.js +0 -0
  264. /package/framework/{v0.6.3 → v0.6.4}/lib/async/package.json +0 -0
  265. /package/framework/{v0.6.3 → v0.6.4}/lib/async/src/main.js +0 -0
  266. /package/framework/{v0.6.3 → v0.6.4}/lib/audit/package.json +0 -0
  267. /package/framework/{v0.6.3 → v0.6.4}/lib/audit/src/main.js +0 -0
  268. /package/framework/{v0.6.3 → v0.6.4}/lib/audit-store.js +0 -0
  269. /package/framework/{v0.6.3 → v0.6.4}/lib/authn/package.json +0 -0
  270. /package/framework/{v0.6.3 → v0.6.4}/lib/authn/src/lockout.js +0 -0
  271. /package/framework/{v0.6.3 → v0.6.4}/lib/authn/src/main.js +0 -0
  272. /package/framework/{v0.6.3 → v0.6.4}/lib/authn/src/totp.js +0 -0
  273. /package/framework/{v0.6.3 → v0.6.4}/lib/authz-gate/package.json +0 -0
  274. /package/framework/{v0.6.3 → v0.6.4}/lib/authz-gate/src/main.js +0 -0
  275. /package/framework/{v0.6.3 → v0.6.4}/lib/cache/README.md +0 -0
  276. /package/framework/{v0.6.3 → v0.6.4}/lib/cache/build.json +0 -0
  277. /package/framework/{v0.6.3 → v0.6.4}/lib/cache/package.json +0 -0
  278. /package/framework/{v0.6.3 → v0.6.4}/lib/cache/src/main.js +0 -0
  279. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/aliases.json +0 -0
  280. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/audit/arguments.json +0 -0
  281. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/audit/help.txt +0 -0
  282. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/audit/verify.js +0 -0
  283. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/add.js +0 -0
  284. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/arguments.json +0 -0
  285. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/build.js +0 -0
  286. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/copy.js +0 -0
  287. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/cp.js +0 -0
  288. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/help.js +0 -0
  289. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/help.txt +0 -0
  290. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
  291. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/list.js +0 -0
  292. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/man.js +0 -0
  293. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/mcp-start.js +0 -0
  294. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/mcp.js +0 -0
  295. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/oas.js +0 -0
  296. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/openapi.js +0 -0
  297. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/remove.js +0 -0
  298. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/rename.js +0 -0
  299. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/restart.js +0 -0
  300. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/rm.js +0 -0
  301. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/start.js +0 -0
  302. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/status.js +0 -0
  303. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/stop.js +0 -0
  304. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/bundle/types.js +0 -0
  305. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/cache/arguments.json +0 -0
  306. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/cache/clear.js +0 -0
  307. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/cache/help.txt +0 -0
  308. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/cache/stats.js +0 -0
  309. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/add.js +0 -0
  310. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/arguments.json +0 -0
  311. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/help.js +0 -0
  312. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/help.txt +0 -0
  313. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/infer.js +0 -0
  314. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/list.js +0 -0
  315. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/migrate.js +0 -0
  316. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/models.js +0 -0
  317. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/remove.js +0 -0
  318. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/rm.js +0 -0
  319. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/connector/test.js +0 -0
  320. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/container/arguments.json +0 -0
  321. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/container/help.js +0 -0
  322. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/container/help.txt +0 -0
  323. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/container/man.js +0 -0
  324. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/container/ps.js +0 -0
  325. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/container/stop.js +0 -0
  326. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/add.js +0 -0
  327. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/arguments.json +0 -0
  328. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/help.txt +0 -0
  329. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/inc/args.js +0 -0
  330. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/inc/namespace.js +0 -0
  331. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
  332. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/inc/reference-scan.js +0 -0
  333. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/inc/scaffold.js +0 -0
  334. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/remove.js +0 -0
  335. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/rename.js +0 -0
  336. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/controller/rm.js +0 -0
  337. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/add.js +0 -0
  338. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/get.js +0 -0
  339. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/help.js +0 -0
  340. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/help.txt +0 -0
  341. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/link-dev.js +0 -0
  342. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/list.js +0 -0
  343. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/remove.js +0 -0
  344. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/rm.js +0 -0
  345. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/set.js +0 -0
  346. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/unset.js +0 -0
  347. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/env/use.js +0 -0
  348. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/add.js +0 -0
  349. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/arguments.json +0 -0
  350. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/build.js +0 -0
  351. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/dot.js +0 -0
  352. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/get.js +0 -0
  353. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/help.js +0 -0
  354. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/help.txt +0 -0
  355. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/init.js +0 -0
  356. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/link-node-modules.js +0 -0
  357. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/link.js +0 -0
  358. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/list.js +0 -0
  359. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/man.js +0 -0
  360. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/msg.json +0 -0
  361. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/open.js +0 -0
  362. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/remove.js +0 -0
  363. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/reset.js +0 -0
  364. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/restart.js +0 -0
  365. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/set.js +0 -0
  366. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/start.js +0 -0
  367. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/status.js +0 -0
  368. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/stop.js +0 -0
  369. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/tail.js +0 -0
  370. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/update.js +0 -0
  371. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/framework/version.js +0 -0
  372. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/gina-dev.1.md +0 -0
  373. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/gina-framework.1.md +0 -0
  374. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/gina.1.md +0 -0
  375. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/helper.js +0 -0
  376. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/add.js +0 -0
  377. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/arguments.json +0 -0
  378. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/export.js +0 -0
  379. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/help.js +0 -0
  380. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/help.txt +0 -0
  381. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/import.js +0 -0
  382. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/i18n/scan.js +0 -0
  383. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/_host.js +0 -0
  384. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/arguments.json +0 -0
  385. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/build.js +0 -0
  386. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/help.js +0 -0
  387. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/help.txt +0 -0
  388. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/list.js +0 -0
  389. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/man.js +0 -0
  390. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/rm.js +0 -0
  391. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/image/run.js +0 -0
  392. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/index.js +0 -0
  393. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/inspector/help.js +0 -0
  394. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/inspector/help.txt +0 -0
  395. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/inspector/open.js +0 -0
  396. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/man-render.js +0 -0
  397. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/minion/arguments.json +0 -0
  398. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/minion/help.js +0 -0
  399. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/minion/help.txt +0 -0
  400. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/minion/kill.js +0 -0
  401. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/minion/list.js +0 -0
  402. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/msg.json +0 -0
  403. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/port/help.js +0 -0
  404. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/port/help.txt +0 -0
  405. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/port/inc/scan.js +0 -0
  406. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/port/list.js +0 -0
  407. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/port/reset.js +0 -0
  408. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/port/set.js +0 -0
  409. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/add.js +0 -0
  410. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/arguments.json +0 -0
  411. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/backup.js +0 -0
  412. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/build.js +0 -0
  413. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/help.js +0 -0
  414. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/help.txt +0 -0
  415. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/import.js +0 -0
  416. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/list.js +0 -0
  417. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/man.js +0 -0
  418. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/move.js +0 -0
  419. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/remove.js +0 -0
  420. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/rename.js +0 -0
  421. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/restart.js +0 -0
  422. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/restore.js +0 -0
  423. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/rm.js +0 -0
  424. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/start.js +0 -0
  425. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/status.js +0 -0
  426. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/project/stop.js +0 -0
  427. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/protocol/arguments.json +0 -0
  428. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/protocol/help.js +0 -0
  429. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/protocol/help.txt +0 -0
  430. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/protocol/list.js +0 -0
  431. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/protocol/remove.js +0 -0
  432. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/protocol/set.js +0 -0
  433. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/add.js +0 -0
  434. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/help.js +0 -0
  435. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/help.txt +0 -0
  436. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/link-local.js +0 -0
  437. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/link-production.js +0 -0
  438. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/list.js +0 -0
  439. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/remove.js +0 -0
  440. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/rm.js +0 -0
  441. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/scope/use.js +0 -0
  442. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/secrets/help.js +0 -0
  443. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/secrets/scan.js +0 -0
  444. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/service/help.js +0 -0
  445. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/service/help.txt +0 -0
  446. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/service/list.js +0 -0
  447. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/service/man.js +0 -0
  448. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/service/start.js +0 -0
  449. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd/view/add.js +0 -0
  450. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd-status-format/package.json +0 -0
  451. /package/framework/{v0.6.3 → v0.6.4}/lib/cmd-status-format/src/main.js +0 -0
  452. /package/framework/{v0.6.3 → v0.6.4}/lib/collection/README.md +0 -0
  453. /package/framework/{v0.6.3 → v0.6.4}/lib/collection/build.json +0 -0
  454. /package/framework/{v0.6.3 → v0.6.4}/lib/collection/package.json +0 -0
  455. /package/framework/{v0.6.3 → v0.6.4}/lib/collection/src/main.js +0 -0
  456. /package/framework/{v0.6.3 → v0.6.4}/lib/config.js +0 -0
  457. /package/framework/{v0.6.3 → v0.6.4}/lib/connector-config/package.json +0 -0
  458. /package/framework/{v0.6.3 → v0.6.4}/lib/connector-config/src/main.js +0 -0
  459. /package/framework/{v0.6.3 → v0.6.4}/lib/connector-error/package.json +0 -0
  460. /package/framework/{v0.6.3 → v0.6.4}/lib/connector-error/src/main.js +0 -0
  461. /package/framework/{v0.6.3 → v0.6.4}/lib/connector-registry/package.json +0 -0
  462. /package/framework/{v0.6.3 → v0.6.4}/lib/connector-registry/src/main.js +0 -0
  463. /package/framework/{v0.6.3 → v0.6.4}/lib/cron/README.md +0 -0
  464. /package/framework/{v0.6.3 → v0.6.4}/lib/cron/package.json +0 -0
  465. /package/framework/{v0.6.3 → v0.6.4}/lib/cron/src/main.js +0 -0
  466. /package/framework/{v0.6.3 → v0.6.4}/lib/domain/LICENSE +0 -0
  467. /package/framework/{v0.6.3 → v0.6.4}/lib/domain/README.md +0 -0
  468. /package/framework/{v0.6.3 → v0.6.4}/lib/domain/package.json +0 -0
  469. /package/framework/{v0.6.3 → v0.6.4}/lib/domain/src/main.js +0 -0
  470. /package/framework/{v0.6.3 → v0.6.4}/lib/dto/package.json +0 -0
  471. /package/framework/{v0.6.3 → v0.6.4}/lib/dto/src/main.js +0 -0
  472. /package/framework/{v0.6.3 → v0.6.4}/lib/dto-pipe/package.json +0 -0
  473. /package/framework/{v0.6.3 → v0.6.4}/lib/dto-pipe/src/main.js +0 -0
  474. /package/framework/{v0.6.3 → v0.6.4}/lib/dto-types/package.json +0 -0
  475. /package/framework/{v0.6.3 → v0.6.4}/lib/dto-types/src/main.js +0 -0
  476. /package/framework/{v0.6.3 → v0.6.4}/lib/generator/index.js +0 -0
  477. /package/framework/{v0.6.3 → v0.6.4}/lib/i18n/package.json +0 -0
  478. /package/framework/{v0.6.3 → v0.6.4}/lib/i18n/src/main.js +0 -0
  479. /package/framework/{v0.6.3 → v0.6.4}/lib/image-build/package.json +0 -0
  480. /package/framework/{v0.6.3 → v0.6.4}/lib/image-build/src/main.js +0 -0
  481. /package/framework/{v0.6.3 → v0.6.4}/lib/index.js +0 -0
  482. /package/framework/{v0.6.3 → v0.6.4}/lib/inherits/LICENSE +0 -0
  483. /package/framework/{v0.6.3 → v0.6.4}/lib/inherits/README.md +0 -0
  484. /package/framework/{v0.6.3 → v0.6.4}/lib/inherits/package.json +0 -0
  485. /package/framework/{v0.6.3 → v0.6.4}/lib/inherits/src/main.js +0 -0
  486. /package/framework/{v0.6.3 → v0.6.4}/lib/inspector-events/package.json +0 -0
  487. /package/framework/{v0.6.3 → v0.6.4}/lib/inspector-events/src/main.js +0 -0
  488. /package/framework/{v0.6.3 → v0.6.4}/lib/inspector-redact/package.json +0 -0
  489. /package/framework/{v0.6.3 → v0.6.4}/lib/inspector-redact/src/main.js +0 -0
  490. /package/framework/{v0.6.3 → v0.6.4}/lib/instrument/package.json +0 -0
  491. /package/framework/{v0.6.3 → v0.6.4}/lib/instrument/src/main.js +0 -0
  492. /package/framework/{v0.6.3 → v0.6.4}/lib/job/package.json +0 -0
  493. /package/framework/{v0.6.3 → v0.6.4}/lib/job/src/main.js +0 -0
  494. /package/framework/{v0.6.3 → v0.6.4}/lib/job-store.js +0 -0
  495. /package/framework/{v0.6.3 → v0.6.4}/lib/json-config-header/package.json +0 -0
  496. /package/framework/{v0.6.3 → v0.6.4}/lib/json-config-header/src/main.js +0 -0
  497. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/README.md +0 -0
  498. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/package.json +0 -0
  499. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/default/index.js +0 -0
  500. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/file/index.js +0 -0
  501. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/file/lib/logrotator/README.md +0 -0
  502. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/file/lib/logrotator/index.js +0 -0
  503. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/containers/mq/index.js +0 -0
  504. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/helper.js +0 -0
  505. /package/framework/{v0.6.3 → v0.6.4}/lib/logger/src/main.js +0 -0
  506. /package/framework/{v0.6.3 → v0.6.4}/lib/math/index.js +0 -0
  507. /package/framework/{v0.6.3 → v0.6.4}/lib/mcp-dispatch/package.json +0 -0
  508. /package/framework/{v0.6.3 → v0.6.4}/lib/mcp-dispatch/src/main.js +0 -0
  509. /package/framework/{v0.6.3 → v0.6.4}/lib/mcp-http/package.json +0 -0
  510. /package/framework/{v0.6.3 → v0.6.4}/lib/mcp-http/src/main.js +0 -0
  511. /package/framework/{v0.6.3 → v0.6.4}/lib/mcp-server/package.json +0 -0
  512. /package/framework/{v0.6.3 → v0.6.4}/lib/mcp-server/src/main.js +0 -0
  513. /package/framework/{v0.6.3 → v0.6.4}/lib/merge/README.md +0 -0
  514. /package/framework/{v0.6.3 → v0.6.4}/lib/merge/package.json +0 -0
  515. /package/framework/{v0.6.3 → v0.6.4}/lib/merge/src/main.js +0 -0
  516. /package/framework/{v0.6.3 → v0.6.4}/lib/metrics/package.json +0 -0
  517. /package/framework/{v0.6.3 → v0.6.4}/lib/metrics/src/main.js +0 -0
  518. /package/framework/{v0.6.3 → v0.6.4}/lib/model.js +0 -0
  519. /package/framework/{v0.6.3 → v0.6.4}/lib/net-locality/package.json +0 -0
  520. /package/framework/{v0.6.3 → v0.6.4}/lib/net-locality/src/main.js +0 -0
  521. /package/framework/{v0.6.3 → v0.6.4}/lib/nunjucks-filters/README.md +0 -0
  522. /package/framework/{v0.6.3 → v0.6.4}/lib/nunjucks-filters/package.json +0 -0
  523. /package/framework/{v0.6.3 → v0.6.4}/lib/nunjucks-filters/src/main.js +0 -0
  524. /package/framework/{v0.6.3 → v0.6.4}/lib/nunjucks-resolver/package.json +0 -0
  525. /package/framework/{v0.6.3 → v0.6.4}/lib/nunjucks-resolver/src/main.js +0 -0
  526. /package/framework/{v0.6.3 → v0.6.4}/lib/proc.js +0 -0
  527. /package/framework/{v0.6.3 → v0.6.4}/lib/release-watch/package.json +0 -0
  528. /package/framework/{v0.6.3 → v0.6.4}/lib/release-watch/src/main.js +0 -0
  529. /package/framework/{v0.6.3 → v0.6.4}/lib/render-cache/package.json +0 -0
  530. /package/framework/{v0.6.3 → v0.6.4}/lib/render-cache/src/main.js +0 -0
  531. /package/framework/{v0.6.3 → v0.6.4}/lib/render-cache-store.js +0 -0
  532. /package/framework/{v0.6.3 → v0.6.4}/lib/routing/README.md +0 -0
  533. /package/framework/{v0.6.3 → v0.6.4}/lib/routing/build.json +0 -0
  534. /package/framework/{v0.6.3 → v0.6.4}/lib/routing/package.json +0 -0
  535. /package/framework/{v0.6.3 → v0.6.4}/lib/routing/src/main.js +0 -0
  536. /package/framework/{v0.6.3 → v0.6.4}/lib/routing/src/radix.js +0 -0
  537. /package/framework/{v0.6.3 → v0.6.4}/lib/routing-introspect/package.json +0 -0
  538. /package/framework/{v0.6.3 → v0.6.4}/lib/routing-introspect/src/main.js +0 -0
  539. /package/framework/{v0.6.3 → v0.6.4}/lib/secrets/package.json +0 -0
  540. /package/framework/{v0.6.3 → v0.6.4}/lib/secrets/src/backends/env.js +0 -0
  541. /package/framework/{v0.6.3 → v0.6.4}/lib/session-store.js +0 -0
  542. /package/framework/{v0.6.3 → v0.6.4}/lib/shell.js +0 -0
  543. /package/framework/{v0.6.3 → v0.6.4}/lib/sqlite-driver.js +0 -0
  544. /package/framework/{v0.6.3 → v0.6.4}/lib/state.js +0 -0
  545. /package/framework/{v0.6.3 → v0.6.4}/lib/swig-filters/README.md +0 -0
  546. /package/framework/{v0.6.3 → v0.6.4}/lib/swig-filters/package.json +0 -0
  547. /package/framework/{v0.6.3 → v0.6.4}/lib/swig-filters/src/main.js +0 -0
  548. /package/framework/{v0.6.3 → v0.6.4}/lib/swig-resolver/package.json +0 -0
  549. /package/framework/{v0.6.3 → v0.6.4}/lib/swig-resolver/src/main.js +0 -0
  550. /package/framework/{v0.6.3 → v0.6.4}/lib/template-loaders/package.json +0 -0
  551. /package/framework/{v0.6.3 → v0.6.4}/lib/template-loaders/src/loaders/http.js +0 -0
  552. /package/framework/{v0.6.3 → v0.6.4}/lib/template-loaders/src/loaders/memory.js +0 -0
  553. /package/framework/{v0.6.3 → v0.6.4}/lib/template-loaders/src/main.js +0 -0
  554. /package/framework/{v0.6.3 → v0.6.4}/lib/url/README.md +0 -0
  555. /package/framework/{v0.6.3 → v0.6.4}/lib/url/index.js +0 -0
  556. /package/framework/{v0.6.3 → v0.6.4}/lib/url/routing.json +0 -0
  557. /package/framework/{v0.6.3 → v0.6.4}/lib/uuid/package.json +0 -0
  558. /package/framework/{v0.6.3 → v0.6.4}/lib/uuid/src/main.js +0 -0
  559. /package/framework/{v0.6.3 → v0.6.4}/lib/validator.js +0 -0
  560. /package/framework/{v0.6.3 → v0.6.4}/lib/watcher/package.json +0 -0
  561. /package/framework/{v0.6.3 → v0.6.4}/lib/watcher/src/main.js +0 -0
  562. /package/framework/{v0.6.3 → v0.6.4}/lib/ws-framing/package.json +0 -0
  563. /package/framework/{v0.6.3 → v0.6.4}/lib/ws-framing/src/main.js +0 -0
  564. /package/framework/{v0.6.3 → v0.6.4}/lib/ws-query/package.json +0 -0
  565. /package/framework/{v0.6.3 → v0.6.4}/lib/ws-query/src/main.js +0 -0
  566. /package/framework/{v0.6.3 → v0.6.4}/lib/ws-session/package.json +0 -0
  567. /package/framework/{v0.6.3 → v0.6.4}/lib/ws-session/src/main.js +0 -0
package/llms.txt CHANGED
@@ -809,7 +809,7 @@ Dev-mode query instrumentation captures every database query tied to the current
809
809
 
810
810
  80. **CSRF — three-phase protection (`#CSRF1` Session plugin / `#CSRF2` signed double-submit token / `#CSRF3` Origin pre-filter)** — Session plugin hardens `express-session` cookie defaults from `settings.json > session.cookie.{sameSite, httpOnly, secure}` (opt-in, one-line bundle bootstrap swap, never transparent because bundles legitimately set `httpOnly: false`); signed token middleware uses `crypto.timingSafeEqual` HMAC-SHA256 bound to `req.session.id` with per-route `routing.json > "csrfExempt": true` opt-out (see `routing-and-http2.md § "Per-route flags"` for `req.routing.csrfExempt` two-step propagation: `lib/routing` extracts → `core/server.js` hoists, top-level on `req.routing` not under `param.*`); Origin pre-filter folded INSIDE `gina.plugins.Csrf()` BEFORE token verify on mutating methods, allowlist via `settings.json > csrf.allowedOrigins` (empty defaults to `[bundleHostname]` auto-derived). Negative-invariant lock: matching token + mismatching Origin still 403s — token layer ≠ Origin layer. Implementation traps from the trilogy: plan-vs-shipped attribute drift (downstream commits must `grep -n '<attribute>' <upstream-source-file>` before referencing — source on develop wins, not the plan); CI flake-vs-regression triage (same-message-different-SHA pairs need `git diff --stat A..B` first); source-inspection `indexOf` matches the function DEFINITION before the call site (use unique assignment LHS like `requestOrigin = parseRequestOrigin(req)`); `Origin: "null"` is a real browser value (sandboxed iframes / `file://`) and needs an explicit `s === 'null'` guard so the parser falls through to Referer instead of 403'ing for "origin not allowed".
811
811
 
812
- 98. **Secrets — `${secret:KEY}` config placeholders + the `secrets:scan`/`secrets:check` introspection CLI (absorbs former #135).** `lib/secrets` substitutes `${secret:KEY}` at config-load (`core/config.js::loadBundleConfig`, post-merge per bundle), resolving each key from the FRAMEWORK environment first (`getEnvVar`) and falling back to `process.env[KEY]`. **That two-tier read is load-bearing, not defensive (0.6.3, #B156):** the CLI MOVES every `GINA_*`/`USER_*`/`VENDOR_*` key OUT of `process.env` into `process.gina` (`filterArgs`, `utils/helper.js`), so a `${secret:GINA_*}` placeholder was previously unresolvable in ANY CLI-loaded config — `mcp.json` (the published example), the connector commands, `audit:verify` — and failed closed with a refusing boot. Non-`GINA_` keys are untouched by the sweep and resolve as before. The same idiom now backs the CSRF `GINA_CSRF_SECRET` tier and the MCP transport's `GINA_BIND_HOST`/`GINA_DIR` reads (#B157). ANCHORED: the regex matches only when the ENTIRE string value is the placeholder — mixed strings (`"prefix-${secret:K}-suffix"`) pass through unchanged; non-string scalars untouched; nested objects/arrays descend. **Fail-closed:** an unset/empty env var throws a GENERIC `Secret resolution failed` — the key name rides only a non-enumerable `_ginaSecretKey`, surfaced at debug level by the config-load catch naming key + `<bundle>/<env>:<scope>` (#B42), so consumer-visible surfaces never carry key names. Secret rotation needs a process restart (the resolver re-runs on `config.refresh()`, but the env is inherited from container init). Pluggable backend interface (`{resolve(key)}` — only the env backend ships); `getResolvedPaths()` WeakMap path tracking for future log-redaction. **`getRequiredKeys(config)`** is the read-only, non-throwing sibling backing the OFFLINE CLI: `gina secrets:scan [<bundle>] [@project]` (which keys each bundle requires, grouped by file — sources derived from the manifest's `src`, never a guessed path) + `gina secrets:check` (marks SET/UNSET, exits non-zero — the CI gate), with `--scope=<s>` overlaying `config_<s>/` dirs READ-ONLY (the runtime loader stays scope-agnostic) and `--env-file` explicit per invocation. Design of record: the framework RESOLVES secrets, the deployment layer STORES them — no stored env-file default. Consumers: `Csrf` (`settings.csrf.secret`), `bundle:mcp-start` (re-resolves `mcp.json` post-parse), `auth.machine` caller keys.
812
+ 98. **Secrets — `${secret:KEY}` config placeholders + the `secrets:scan`/`secrets:check` introspection CLI (absorbs former #135).** `lib/secrets` substitutes `${secret:KEY}` at config-load (`core/config.js::loadBundleConfig`, post-merge per bundle), resolving each key from the FRAMEWORK environment first (`getEnvVar`) and falling back to `process.env[KEY]`. **That two-tier read is load-bearing, not defensive (0.6.3, #B156):** the CLI MOVES every `GINA_*`/`USER_*`/`VENDOR_*` key OUT of `process.env` into `process.gina` (`filterArgs`, `utils/helper.js`), so a `${secret:GINA_*}` placeholder was previously unresolvable in ANY CLI-loaded config — `mcp.json` (the published example), the connector commands, `audit:verify` — and failed closed with a refusing boot. Non-`GINA_` keys are untouched by the sweep and resolve as before. The same idiom now backs the CSRF `GINA_CSRF_SECRET` tier and the MCP transport's `GINA_BIND_HOST`/`GINA_DIR` reads (#B157). ANCHORED: the regex matches only when the ENTIRE string value is the placeholder — mixed strings (`"prefix-${secret:K}-suffix"`) pass through unchanged; non-string scalars untouched; nested objects/arrays descend. **Fail-closed:** an unset/empty env var throws a GENERIC `Secret resolution failed` — the key name rides only a non-enumerable `_ginaSecretKey`, surfaced at debug level by the config-load catch naming key + `<bundle>/<env>:<scope>` (#B42), so consumer-visible surfaces never carry key names. Secret rotation needs a process restart (the resolver re-runs on `config.refresh()`, but the env is inherited from container init). Pluggable backend interface (`{resolve(key)}` — env-only by default, env-over-file once `secrets.file` is declared); `getResolvedPaths()` WeakMap path tracking for future log-redaction. **FILE SYNTAX — one parser (`lib/secrets/src/env-file.js::parseEnv`) serves BOTH readers (`secrets:check` + the file backend), so the gate cannot green-light a file the runtime reads differently.** It targets what a POSIX shell `source`s, since the documented entrypoint (`set -a; . secrets.env; set +a`) is a THIRD reader the module cannot unify by construction. **#B269 (0.6.4) — a trailing `#` comment is now stripped** (previously swallowed into the value): the strip fires only when the `#` is preceded by whitespace AND outside quotes, because `abc#def` is a literal hash to a shell and `"abc # def"` is a quoted one — so quoting is a COMPLETE escape hatch, and the strip runs on the RAW pre-trim value (trimming first makes `KEY= # c` and `KEY=#c` indistinguishable while the shell reads them as `''` vs `'#c'`). Unquoting applies AFTER the strip, so `KEY="abc" # note` is `abc`, not `"abc"` — previously the comment defeated the `/^".*"$/` test and the quotes became part of the credential. Consequence to expect: `KEY= # comment` is now EMPTY, and empty counts as unset in both readers (`typeof v === 'string' && v !== ''`), so it fails closed and `secrets:check` reports UNSET where it previously reported a FALSE-GREEN SET. Two divergences REMAIN by decision — `KEY = value` (accepted+trimmed here, a shell errors) and `KEY="a"b"` (`a"b` here, `ab` in a shell) — so do NOT restate the parser as "whatever a POSIX shell does". **`getRequiredKeys(config)`** is the read-only, non-throwing sibling backing the OFFLINE CLI: `gina secrets:scan [<bundle>] [@project]` (which keys each bundle requires, grouped by file — sources derived from the manifest's `src`, never a guessed path) + `gina secrets:check` (marks SET/UNSET, exits non-zero — the CI gate), with `--scope=<s>` overlaying `config_<s>/` dirs READ-ONLY (the runtime loader stays scope-agnostic about config DIRS) and `--env-file` standing in for the ENV TIER. **#B263 (0.6.4) — `check` now reads BOTH runtime tiers, in order:** env, then the bundle's declared `secrets.file` chain resolved through the SAME global `whisper` + the shared `parseEnvFile`, so it can no longer report UNSET (and exit non-zero) for a key the bundle would have booted with — that false RED, not a false green, was the dominant real-world defect, since adding a source can only turn UNSET→SET. Parity is BY CONSTRUCTION: `homedir` comes from the project's own `env.json` (`<bundle>.<env>`) else the template's `~/.<projectName>`, NEVER `projects.json`'s `homedir` (a CLI-level field `config.js` never reads). A reps key is seeded only from a NON-EMPTY string — an absent key leaves its token verbatim and is caught loudly, but an EMPTY one substitutes SILENTLY (measured: `scope:''` → `/h//f.env`, which collapses to the base path on POSIX with no guard firing; whisper's pass 2 `dict[k]||s` leaves it, pass 3 `!==undefined` rescues it). `${scope}`/`${env}` are LAUNCH-time values the CLI cannot read (runtime prefers `NODE_SCOPE`/`NODE_ENV`), so the report NAMES the values it assumed, `--env` was added to pick the env block, and an unresolvable path is reported + its tier skipped rather than statted — under-reading can only make the gate stricter than the runtime, never laxer. **#B266 (`74e52cda`) — the reps dict must carry every token a REAL chain uses, not the ones the examples use.** The first consumer config (`${homedir}/v${projectVersionMajor}/credentials/[${scope}/]secrets.env`) resolved at runtime but NOT in `check`, re-opening the #B263 false-RED through three holes: (a) **version tokens absent** — the runtime does not list them in the bundle-config dict either, it gets them because `core/template/conf/env.json` declares `projectVersion`/`projectVersionMajor` as per-bundle SCALARS which the `:2356` harvest then copies in, so **reading only the `:2296` dict literal shows no version tokens and looks complete**; now seeded from the manifest; (b) **`${scope}` had no default** — now falls back to the project default via a SEPARATE `self.scopeAssumed` slot, because defaulting `self.scopeName` would start applying the `config_<scope>/` overlay to every bare invocation (two axes, one name); (c) **`~/.undefined`** — `buildReps` used `self.projectName`, undefined on the all-projects branch, yielding a real stat-able wrong path; now derived from the project PATH. Declaration resolution is pinned (`5a80b1a1`): bundle `secrets.file` REPLACES shared's (arrays never concatenate), `secrets:{}` INHERITS, `secrets:{file:null}` is the opt-out, and `--env-file` stays TOP tier (it stands in for the environment; demoting it below the files would model something the runtime never does). Each key's report names the tier that satisfied it. ⚠️ The other `secrets.resolve()` callers pass NO backend and stay env-only (`bundle:mcp-start`, `connector:infer`/`test`/`models` — #B264). Design of record: the framework RESOLVES secrets, the deployment layer STORES them. Since 0.6.4 it may additionally READ declared plaintext files as the LOWEST-precedence tier — opt-in `settings.secrets.file` in a bundle's `settings.json`, one path or an array written with the usual config tokens (`["${homedir}/secrets.env", "${homedir}/${scope}/secrets.env"]`), later entries winning. Files layer UNDER the environment (`getEnvVar` → `process.env` → files → throw), which INVERTS the usual `.env` intuition and is deliberate: every production delivery path (K8s `secretRef`, ECS task secrets, `sops exec-env`, a CI export) arrives via the environment, so a file that won could let a stale plaintext copy shadow the real credential. ⚠️ Precisely, it is a NON-EMPTY env value that wins (#B268, `90518bec`): a SET-but-EMPTY variable counts as ABSENT and the file fills it — deliberate and kept, because `environment: ["X=${X}"]` with the outer var unset puts an empty-but-SET var in the container (measured) and refusing would break working setups — but since a failed `export X="$(fetch …)"` produces that same shape, the fall-through WARNS, naming the key and never the value, at `warn` so the default `info` hierarchy keeps it. Undeclared ⇒ byte-identical to env-only; a declared-but-ABSENT file contributes nothing, but one that EXISTS and cannot be READ is FATAL (#B267, `90518bec`) — collapsing both to `null` let a `[base, per-scope]` chain whose per-scope file lost read permission boot silently on the SHARED credential (measured: `production_password` → `shared_dev_password`), announced only by a debug line MISLABELLED `ABSENT` which `info` suppresses; `env-file` now also exports `readEnvFile` (reports the errno — ENOENT vs EACCES/EISDIR; a dangling symlink is ENOENT, correctly absent), the backend refuses on anything non-ENOENT naming path + code, and `secrets:check` reports `UNREADABLE` so the CI gate and the runtime cannot disagree about a file neither can open; resolution stays fail-closed; paths carrying an unresolved `${…}` or a `${secret:…}` are rejected at boot. Selection reads `content.settings`, NOT `settings` — `config.js` binds `.settings` BEFORE the token-substitution pass and `.content` after, and substitution returns a NEW object, so only the content copy has resolved tokens. It does NOT decrypt: for SOPS/Vault/KMS keep decrypting at the container entrypoint — a structural constraint, not a preference, since `lib/secrets` is fully synchronous and `config.js` holds the #P33 sync-init contract, so a network-backed backend would put a KMS round-trip inside a synchronous boot path. (The scope-agnostic note above is about `config_<s>/` DIRS, which the runtime still does not overlay; a secrets FILE path may be scope-varying via `${scope}`.) Consumers: `Csrf` (`settings.csrf.secret`), `bundle:mcp-start` (re-resolves `mcp.json` post-parse), `auth.machine` caller keys.
813
813
 
814
814
  125. **CLI handler authoring & operations — consolidated** (replaces individual entries #17, #23, #24, #55, #56, #58, #59, #60, #62, #63, #102, #112; plus #161, #166, #167, #181, #182, #183, #185, #199, #201, #202, #203 folded 2026-07-05):
815
815
  - **CLI stubs** — `gina --status` / `gina -t` appear in help.txt or docs but have no handler (not in `aliases.json`); tracked in `ROADMAP.md § CLI`; never suggest to users without checking the handler file first. `bundle:status` / `project:status` / `minion:list` / `minion:kill` / `protocol:remove` / `bundle:copy` (+ `cp` alias) / `bundle:rename` shipped 0.4.1-alpha.2; `project:move` / `project:backup` / `project:restore` / `framework:update` / `framework:man` (+ `project:man` / `bundle:man` / `service:man`) — the **CLI Tier 3** finals — shipped 0.5.x (full coverage in the two CLI Tier 3 sub-bullets below). Both minion commands are run-dir-driven process-truth (the "minion" abstraction is half-wired — nothing sets `process.isMinion` or writes `*minion*.pid`, so a minion == any running bundle child-process): `minion:list` lists every live `<bundle>@<project>.pid` grouped by project via `lib.cmdStatusFormat`; `minion:kill @<project>` reaps them (hybrid kill-set = run-dir pidfiles + a `ps -ef | grep 'gina: ...@<project>'` sweep for pidfile-less orphans bundle:stop misses), SIGTERM→grace→SIGKILL escalation, `--dry-run` preview, unlinks stale/killed pidfiles, never touches mount symlinks or its own PID. `protocol:remove <bundle> @<project>` reverts a bundle to the project default protocol by deleting ONLY its `server.protocol/scheme/allowHTTP1` override from the bundle's `settings.json` (config.js `:1014/:1020` auto-defaults an absent protocol to `def_protocol`/`def_scheme`); it deliberately does NOT mutate the shared `ports*.json` — `project:add` pre-allocates the full protocol×scheme×env matrix, so the default-protocol port already exists and pruning the set's port would be wrong; a per-env port-presence guard refuses (unless `--force`) when the default-protocol port is missing; `--dry-run` preview, header-preserving JSON rewrite (connector:rm pattern). `bundle:copy <source> <new> @<project>` (+ `cp`) duplicates a bundle under a new name in the SAME project: copies the `src/<source>` tree, then word-boundary-rewrites the name footprint (PascalCase `<Src>`→`<Dst>` for controller class names + lowercase whole-word `<src>`→`<new>` for the gina require-var / `app.json` name / webroot path, `.js`/`.json` files only — embedded tokens like `apiClient` are untouched; a first-bundle webroot `/` is repointed to `/<new>`), allocates a fresh FULL protocol×scheme×env port matrix via the shared `setPorts` (a single-port insert would later emerg in `config.js`, which expects the complete matrix), and clones+repoints the source's manifest entry (`src`/`link`/`releases` target paths). `--dry-run` previews every rewrite site before writing; `--force` overwrites an existing target (its `removeDest` mirrors `bundle:remove`'s deletions). TWO positionals leave `self.name` null (CmdHelper sets it only for a single positional), so the handler reads `self.bundles[0]`/`[1]` directly — which also slips past the `cmd.name`-gated existence guard so the not-yet-registered new name isn't rejected. `bundle:rename <old> <new> @<project>` is the move-sibling — it renames a bundle IN PLACE in the same project (`fs.renameSync` move, NOT a copy) reusing the same `inc/name-rewrite.js` engine but with `fixWebroot:false` (rename moves the only bundle, so there's no first-bundle/collision case; a name-derived `/<old>` webroot is still rewritten by the lowercase pass). Its ports are REKEYED, not reallocated — port NUMBERS are preserved: `ports.json` rewrites the `<old>@<project>/` owner prefix back into the SAME `[protocol][scheme][portKey]` slot (avoiding the two `project/rename.js` bugs: the wrong `[protocol][portKey]` slot, and a project-wide owner replace), then `ports.reverse.json` is rekeyed (`pr[new]=pr[old]; delete pr[old]`) and flipped LAST as the canonical existence record. It REFUSES a running bundle with NO `--force` bypass (`--force` only overwrites an existing dest); the whole multi-surface mutation (symlink → renameSync dir → rewrite tree → env → manifest → ports → ports.reverse) is snapshot-guarded (the `bundle:add` rollback model) so any post-move failure reverses the dir move and restores env/manifest/ports/ports.reverse from in-memory snapshots. NOTE: the ROADMAP's "fix the help.txt remouve typo" item was stale — no such typo existed.
@@ -892,7 +892,7 @@ Dev-mode query instrumentation captures every database query tied to the current
892
892
 
893
893
  206. **Observable application events surface in the dev Inspector via a per-request signal that mirrors #AISTREAM — `self.emitEvent(name, metadata)` (controller) or `lib/inspector-events.emit()` (model/service code) pushes a `{type:'event',id,name,t[,meta]}` entry and emits a live `inspector#event` frame, delivered as an `event: event` SSE/WS frame plus a `user.events` end-of-request snapshot, shown in a new SPA "Events" tab (#EVTBUS, 2026-06-29).** The per-request buffer `_devEventLog` is a THIRD key on the shared `process.gina._queryALS` store (sibling of `_devQueryLog`/`_devAiLog`, seeded at `controller.js` `setOptions`); `emit()` reaches it via `getStore()` so the snapshot is captured from any code in the request's async context (outside one — a background job / lifecycle hook — the live frame still fires but no snapshot is pushed, per Slice 2b below; a closed gate or invalid name is a no-op). Capture is gated on `NODE_ENV_IS_DEV` OR an open `process.gina._inspectorWindowUntil` (identical to the query/AI gates). The event NAME + framework stamps always ride the wire; the caller's `metadata` VALUES ride ONLY when `settings.inspector.events.captureArgs` is true (default off, seeded onto `process.gina._inspectorEventsCaptureArgs` at boot) — `lib/inspector-redact` matches secret-NAMED keys only and cannot sanitise arbitrary arg VALUES, so the gate + opt-in + authenticated channel are the protection, never redaction (same contract as #AISTREAM `captureText`). Delivery: a `server.isaac.js` SSE forwarder (`event: event`) + a `server.js` WS forwarder (`{event:'event'}`), registered/deregistered beside the data/log/token listeners; the `user.events` snapshot is attached at the SAME 5 render sites as `user.aiStream` (render-json + inspector-window-emit on `_gdUser`, render-swig cache-hit/miss + render-nunjucks on `data.page.events`), gated on `local._eventLog.length`. **Three carry-forward facts:** (1) the SPA `appendAppEvent` ACCUMULATES a capped rolling buffer (NOT single-slot-reset like `appendTokenDelta` — events are discrete, not one token stream), renders via `.textContent` (untrusted app text), and the `renderTab` `case 'events':` prefers the live buffer then falls back to the request snapshot; (2) the live `event: event` frame rides isaac-SSE + server.js-WS, and (#AISTREAM/#EVTBUS parity gap CLOSED 2026-06-29) the server.js HTTP/1 SSE `/_gina/agent` handler now forwards token + event too via `response.write` (mirroring isaac's `_agWrite` shape) — all three live transports now carry all four frame types and `event-inspection.test.js §08` is a positive pin; (3) `event` is NOT an EventSource-reserved name (unlike open/message/error) so `event: event` is collision-free. Adding the 8th SPA tab updates the 3 `TAB_LAYOUTS` presets → trips `inspector.test.js §44`'s exact-tab-set pins (now 8 tabs, updated with approval). `lib/inspector-events` registered via `_require` (stateless). There are zero pre-existing app/domain events in the framework (the only `process.emit` namespaces are `inspector#`/`logger#`/`gina#`, all internal), so the emit API is the headline — without it the signal surfaces nothing. Slice 2a (SHIPPED 2026-06-29): a curated allow-list (`settings.inspector.events.topics`, default `[]`) bridges entity-trigger emits onto the live signal via a gated block in `entity.js`'s `emit` chokepoint — skips `error`, gated on a non-empty allow-list FIRST (the opt-in IS the flood control), ships a framework-controlled `{ok,error}` summary (never raw rows; captureArgs-gated like all metadata — name+source always ride, the `{ok,error}` summary reaches the wire only when captureArgs is on, default off) tagged `source:'framework'` (a new optional 3rd arg to `inspector-events.emit` + an exported `matchTopics` helper: exact / single leading-or-trailing `*`); reached via `require('lib/inspector-events')`; request-scoped so it reuses the MVP live+snapshot delivery (no new transport/ALS key/tab). The chokepoint catches every entity emit with ZERO per-instance `.on()` / `ENTITY_MAX_LISTENERS` use (the original deferred note had conflated flood with listener-exhaustion). Only the couchbase connector emits named CRUD (`N1QL:<entity>#<method>`); the other 5 don't, and CRUD is largely redundant with the Query tab — so 2a's genuinely-new value is custom (non-query) entity methods. Slice 2b (SHIPPED 2026-06-29): an emit refactor SPLITS the store-gated buffer push from an always-on live emit — once the gate passes the live `inspector#event` frame fires store-or-not (a no-store background-job / lifecycle caller now reaches the stream; only the per-request snapshot needs a store), return `true`⟺live-frame-emitted / `false` only for gate-closed or invalid-name; this flipped §01 (`:58-63`) and CHANGED the no-op-outside-request contract, deliberately diverging from #AISTREAM (whose live `inspector#token` emit is itself store-gated at `ai/index.js` `if(_aiLog)`). On top of it a connector-lifecycle bridge surfaces a connector's recurring `ready` emit (re-fired on every reconnect) via a single additive `connector.on('ready')` at the construct-once site in `core/model/index.js this.connect` (attached once in the cache-miss branch → no per-reconnect accumulation; the existing `onReady` consumer is a one-shot `once('ready')`; couchbase is the only connector emitting `ready` today, the other 5 use a direct onReady callback; redis session-store connect/disconnect — the only other recurring pair — has no framework seam so it is deferred), live-only (lifecycle events have no request context → no snapshot, exactly the no-store path the refactor unblocked), reusing the same `_inspectorEventTopics` allow-list + `matchTopics` + `source:'framework'` tag + `{ok,error}` summary (no new config key). Tests: `test/core/event-inspection.test.js` (§01 real-module behaviour incl. the no-store live-emit, §02-§09 server wiring, §10-§11 SPA + dist propagation, §12-§14 Slice 2a: source/matchTopics + entity-bridge source-pins/replica + topics seed, §15 Slice 2b connector-lifecycle bridge: model/index.js source-pins + replica). Slices A1a `b2157113` / A1b `bb3103f1` / A1c `4f844bd6`; Slice 2b emit refactor `ea55a489` + connector-lifecycle bridge. Slices 2a + the 2b always-on-emit substrate LIVE-VERIFIED e2e 2026-06-30 (entity op → `inspector#event` frame + `user.events` snapshot; a no-store background emit → live frame), which also corrected the captureArgs note above; the couchbase-specific `couchbase#ready` reconnect→frame e2e is source + unit-replica only (no self-controlled couchbase env).
894
894
 
895
- 209. **FormValidator — engine disambiguation, string inputs, a11y reflection, and live-check message visibility (consolidates former #42/#130/#150/#186/#197).** The live form/data rule engine is `core/plugins/lib/validator/src/form-validator.js` (single source, `isGFFCtx`-branched: runs server-side via `backendInit` AND compiled into the browser bundle) with the client orchestration in `validator/src/main.js` — `framework/v*/lib/validator.js` is a DEAD standalone fluent validator with overlapping `is*` rule names; never edit it for form-rule work (tell them apart fast: the live engine's `isRequired` rejects whitespace-only input, the dead one passes it). Rule bodies must handle STRING inputs — the two DECLARATIVE contexts feed strings (`.value` + urlencoded bodies) — so a typed/numeric rule coerces or parses explicit components, never assumes a typed JS value; but "always a string" is NOT true of every path, and reading it that way shipped #B198 (see below): a JSON request body keeps real Numbers (`JSON.parse` → `req.body`/`req.post`, which the `validator::{}` routing path MERGES into the validated data before spreading array bounds through `apply()`), and `toInteger` leaves `Math.round()`'s real Number on `this.value`, so a `toInteger` → `is*` chain hands the next rule a Number even in the browser — a rule must therefore be correct for a typed value too, not merely tolerant of strings: `isFloat` coerces via `Number()` (#B46); `isDate` builds from explicit mask components + a round-trip check so non-ISO slash masks aren't US-misparsed and impossible dates still reject (#B47); `isDate` returns the FIELD again on its valid path (#B48, 0.5.4 — parsed `Date` preserved on the field's `.value`, the `isDate(mask).format(...)` idiom unchanged), so rule chaining works. An empty value is adjudicated by `isRequired` ALONE (#B78): the per-rule empty-bypass became unconditional on empty (`if (this.value == '')`, its old `!errors['isRequired']` gate dropped) for `isEmail`/`isJsonWebToken`/`isFloat`/`isInList` — each regating `this.valid = isValid && !errors['isRequired']` — and `isString` keeps the field invalid without recording a second message, so a required-empty field shows ONE message (`is required`) not two, optional empty fields still pass, a filled-but-invalid value still reports its own error, and custom `is` was deliberately excluded by #B78 and re-declined by #B82 — an exclusion REVERSED by #B233 (2026-08-03, unreleased, `307721f2`): `is` now carries the same canonical strict bypass (`if ( this.value === '' ) { isValid = true; }`) and the same regate, taking the Shape-A population from four rules to FIVE, so a required+EMPTY field carrying an `is` condition records `isRequired` ALONE instead of also collecting a second `Condition not satisfied`. `isBoolean` joined the same contract at #B235 (2026-08-03, unreleased, `aa1c2035`), taking that population to SIX: its pre-switch rescue `errors['isRequired'] && this.value == false` was LOOSE (`'' == false`), so a required+EMPTY boolean field LOST its isRequired error and reported `Must be a valid boolean` instead of `Cannot be left empty`; the rule now takes the canonical strict `=== ''` self-pass, and the rescue moves AFTER the accept-set switch gated on the value having been ACCEPTED (`val !== null`) — which keeps the documented unchecked-but-required-toggle case working, since a recognized `false`/`0` is a present answer, while emptiness returns to `isRequired` alone. Paired in the same commit with #B236, the SERVER-side half: the plugin's `getCastedValue` funneled EVERY value on an isBoolean-ruled field through `/^true$/i ? true : false` BEFORE the engine ran (client AND server — `validate` calls `formatFields` unconditionally), so on the server auto path junk validated CLEAN and PERSISTED as `false` — `nope`, the HTML checkbox default `on` (a CHECKED box storing UNchecked), the strings `1`/`0`, `TRUE`/`True` — and the NUMBER 1 stored `false` where the engine reads it as `true`. The pre-cast now survives ONLY in dynamised-rules mode, where a referenced boolean field must splice into a stringified `is` condition as an unquoted operand (measured NECESSARY: deleting it outright breaks a server `$flag === true` condition); the ENGINE is the single adjudicator on every surface, which is what the routing `validator::` surface always enforced and what the published reference already promised. Disclosed both directions: values that silently stored `false` now ERROR, the number 1 flips its stored value `false`→`true` on a verdict that was already valid, an optional blank boolean field now PASSES instead of erroring, and a required blank field's message changes from isBoolean to isRequired. A sibling server-path crash in the same plugin is fixed by #B234 (2026-08-03, unreleased, `7c56565d`): `getDynamisedRules` substitutes in two passes, and the SECOND is a DOM fallback re-deriving each splice value from the live element (`$fields[...].value`) — which `backendInit` calls with `$fields = null`, so it threw `TypeError: Cannot read properties of null` on its FIRST iteration for ANY `$` surviving pass 1: a regex end-anchor in an `is` condition, a `$` inside a human-readable message string, or a `$` in any array-rule element after the first. Plain cross-field `$peer === $me` never crashed, because pass 1 consumes tokens that NAME fields. The loop is now gated `$fields && ...`, joining the #B127 precedent one function later; `validate`'s same-text gate is deliberately left UNGUARDED, being reachable only with a live DOM. Residual, disclosed — and since FIXED (#B239): a `$` token in an ARRAY rule's FIRST argument that names no field (`isInList: ['$100']`) threw one site later at `checkFieldAgainstRules`' `d[<token>].value` — NOT DOM-dependent, so it reached the client too. The substitution is now gated on the token resolving to a REAL field (an existing `d` key with a defined `.value` — two clauses, both load-bearing: an engine-METHOD-name collision like `'$isValid'` resolves to a defined key with no `.value`, and pre-fix spliced the string "undefined" into the rule for a silent wrong verdict rather than a crash); anything else stays LITERAL so strict comparison applies (`'$100'` matches its own literal, rejects non-members with the rule's own error; bare-`$` and mixed elements covered). `$` is therefore the engine's RESERVED cross-field sigil: whether an authored `$` stays literal depends on a runtime field-name collision — a token naming a sibling field is consumed UPSTREAM by getDynamisedRules loop 1, substituted with quoting fit for `is`-condition splices, not array elements (`"yes"` with quotes can never match `yes`), so real cross-field refs in array-rule elements are always-invalid, fail-closed, never-worked, undocumented (the reference scopes `$name` to `is` expressions) — tracked as #B240 (demand-gated; the fix is relocating array-element substitution into checkFieldAgainstRules, whose guarded loop is deliberately preserved as the substrate). This reserved-sigil model is also the #DTO2 `$` guard's CURRENT rationale (the crash rationale is retired — deterministic literal semantics are impossible for any `$`, so toRules() refuses at boot rather than validate collision-dependently). The reversal is measured rather than re-argued: the old bypass was gated on `!errors['isRequired']` — off exactly when #B78 wants it on — beside a two-disjunct guard that was DEAD CODE (`x == '' && x != 0` has no witness), optional+empty ALREADY self-passed through the live else-if, and on required+empty the condition is VERDICT-IRRELEVANT (form validity is `getErrors().count()` and `isRequired` has already errored), so form validity and the request payload are identical in both directions and only the message list changes; the "coercion-sensitive" premise had already been retired by #B199's strict test, which leaves `0`/`false`/`null` as operands that still evaluate the condition. #B82 is neither regressed nor retired — its root `getCastedValue` quoting and its `is()` grammar guard stay necessary and reachable with a FILLED host; #B233 only closes that crash path a second time for an empty HOST, whose condition is no longer compiled at all. Same commit drops the dead `_defaultErrorLabels['isApiError']` entry (zero consult sites: the API path assigns the server's message directly and never calls `replace()`) - but a cross-field `is` (`"$a === $b"`) no longer THROWS when the referenced field is empty (#B82): the client dynamised-rules substitution (`getCastedValue` in `main.js`) now renders an empty referenced operand as a quoted `""` (it was spliced RAW, leaving a dangling `"7654321" === ` that `is()`'s binary-comparison grammar `_SCS_BINARY_RE` rejected -> an uncaught throw that aborted the whole-form validity pass and left the submit trigger ungated on an invalid form, breaking the documented `is`+`isRequired` value-confirmation pattern while the confirm field was blank), mirroring `getDynamisedRules`' own sibling substitution default (`: '\"\"'`); `null`/`undefined` stay raw (already valid operands). Hardening: `is()`'s grammar mismatch now FAILS-THE-FIELD (`console.warn`+`isValid=false`) instead of throwing, so a per-keystroke live check can never abort the gate on a residually-unparseable condition (e.g. a field literally valued `"NaN"`, which the root fix leaves raw). Browser-bundled -> prod dist rebuilt; the `#SCS1e`/`#SCS1h` eval-safety pins target the untouched `_SCS_BINARY_RE`/`_scsParseOperand`/regex-literal constructs, so the hardening flips none of them. The form's validity comes from `getErrors().count()` (the surviving `isRequired` error), never the per-field `.valid` flag (whose only error-dropping reader, `setErrors`, is dead). Length bounds are ARITY-sensitive, and the source JSDoc was WRONG about it until 0.6.3: `"isString": [N]` (same for `isInteger`/`isNumber`) supplies `minLength` ONLY — identical in effect to the scalar `N` — because the exact-length branch fires only when `minLength === maxLength`, so an exact length needs `[N, N]`; the stale comment had propagated verbatim into the published reference page, so correct BOTH surfaces when one is found. Those bounds measure the value's STRING FORM (`val.toString().length`) — until 0.6.3 `isInteger` alone measured a bare `val.length`, which is `undefined` on a real Number, so BOTH its bounds were silently inert on every numeric value: no error, no warn, field left `valid` (#B198, a fail-OPEN bypass reachable from a JSON body, a `validator::{}` requirement, or a preceding `toInteger` — the browser included). `isString` reads the same bare `val.length` at two sites and is CORRECT there because a `typeof(val) == 'string'` guard precedes it, so this class of fix is line-scoped: a whole-file replace of the bound expression hits four sites, two of which must not change. One consequence of measuring the string form, intended: a negative number counts its sign toward the length (parity with the same value arriving as a string). The zero-swallow residual #B198 initially left open is CLOSED by #B199 (0.6.3): loose `== ''` emptiness tests conflated `0`/`-0`/`false`/`[]` with the empty string at FIVE sites — the isInteger/isNumber bounds gates AND the isEmail/isJsonWebToken/isFloat empty-bypasses, where a JSON body's `{"email": 0}` validated as a correct email — all five now compare strictly, so only the literal `''` bypasses (the designed empty-is-adjudicated-by-isRequired contract, preserved byte-exactly); `isString` stays loose behind its typeof guard (operators identical for strings), `isInList` was already strict, and `isDate`'s broader `!val` swallow (a silent half-state: `valid` false, NO error recorded, so the form passes) is deliberately untouched. STILL OPEN sibling (#B200): a TRUTHY non-string in an isEmail/isJsonWebToken field (`{"email": 123}`) hits an unguarded `.toLowerCase()` and the rule driver RE-THROWS, killing the whole validation run — the falsy/truthy non-string space is partitioned between the fixed bug and this one. Custom validators (`bundle/validators/<name>/main.js`) are a BROWSER-ONLY affordance, NEVER a server-side guarantee: the server gate reads `getContext('gina').forms` while the loop it guards reads a bare `gina` that is undefined in Node, so a custom rule never attaches server-side and the engine then silently skips the unknown rule name with no warn — re-validate such constraints in the action. (Publishing that context without also fixing the loop would make EVERY validator construction throw, including for bundles shipping no custom validators.) A rule-body edit needs a prod dist rebuild AND flips the section-locked characterization tests by design. Editing trap: `form-validator.js` embeds hidden NO-BREAK SPACE bytes (U+00A0) where a normal space appears inside several `||`/ternary sequences, so a literal-space find/replace spanning one silently fails — patch such regions with a byte-scoped script over clean-ASCII substrings, not a space-spanning match. The blur-time global validation pass sets submit-button state but renders errors ONLY for the touched field — untouched invalid fields stay quiet until interacted-with or submit. Accessibility (#A11Y1): the rule-agnostic chokepoint `handleErrorsDisplay` reflects committed errors into `aria-invalid="true"` (gated on committed-not-warning; `"false"` on clear mirrors native `ValidityState` so it agrees with `:user-invalid`; hidden fields skipped), auto-wires `aria-errormessage` to a gina-owned message div UNLESS the consumer provided their own, focuses the first DOM-order invalid field on a failed submit, and announces blur-time errors via a per-form visually-hidden `aria-live="polite"` region; the per-field aria passes fire only under live-check — the always-on submit pass covers every bound form regardless. Error-MESSAGE visibility has THREE write paths (create / refreshWarning's un-hide / the refresh re-create) and the re-create runs LAST in the live-check pass, so it owns the steady state: it is focus-aware — message hidden while the edited field is the active element, revealed on blur (soft warning border while typing). Rule: when an element is written by multiple paths in a single validation pass, guard the LAST writer — an earlier-writer fix is silently overridden. The invalid submit trigger is marked `aria-disabled="true"` + the class `gina-form-submit-disabled`, NEVER native `disabled` (#B76 — a natively-disabled button emits no click, so the validate-render-focus guard could never run); `isValid()` is the real send gate, and **consumers style that state — the framework ships no button CSS**. Form-associated custom elements (FACEs) participate in binding + live-check (#CC2 — hyphenated members of `form.elements`; their own `.value` accessor is honoured, live-check rides the composed bubbling `change`; author contract: `static formAssociated`, a `name` attribute, a `.value` getter, composed `change` on commit). **Radio-group collection (#B221):** an unchecked non-boolean radio group whose rule declares a truthy `isRequired` is collected as an EMPTY value by BOTH collectors (`getFormValidationInfos` + the native-submit inline copy) so `isRequired` adjudicates it via the standard emptiness test — pre-fix no collection arm admitted the shape (each required `.checked`, a `true|false`-shaped value, or an `isBoolean` rule), the DOM handle was held in `$fields` but the VALUE never entered `fields`, so no rule ran against the group and a radio-group-only form short-circuited BOTH submit guards (field count 0 reads as nothing-to-validate → synthetic `isValid() === true`) and submitted its XHR with zero client-side validation. Other unchecked groups stay absent-when-unchecked (native parity: no rule / `isRequired: false` unchanged; `isBoolean`-declared groups keep the force-false arm), checked members post exactly as before, and on the auto path a required-empty form is invalid and never sends — so the wire only changes for the newly-gated shape. Enforcement-tightening: forms that silently submitted with nothing picked now gate on the pick (trigger `aria-disabled` at bind under default-on live-check, message on submit attempt, re-enabled after picking). **The re-enable is real only since #B228:** the radio live-check listener was registered under a `changed.<id>` event name nothing dispatches on a user pick — the form-level click proxy short-circuits into the radio state updater (which never dispatches any gina event), and the change proxy dispatches ONLY names present in the event registry, which radios never registered (checkboxes have that registration via their state-updater relay; radios' equivalent relay is keyed on the bare element id, which nothing triggers) — so the whole-form silent pass never re-ran after a pick and the trigger kept its bind-time disabled state indefinitely, while submit-time validation (a separate call chain) accepted the checked group and let the click-guard send: flows completed, only the trigger state was wrong (announced disabled to assistive tech; automation actionability checks refuse `aria-disabled`). Radios now ALSO register the proxy-dispatched `change.<id>` name alongside `changed.<id>` — the handler's radio arm accepted `change.`-typed events all along, so one registration line closes the loop: mouse, label and keyboard picks all re-run the field + whole-form passes (single delivery per pick — native `change` fires only on real state changes; the legacy `changed.<id>` name stays registered for the relay/programmatic path; checkboxes byte-identical). Latent since the live-check's introduction, invisible until #B221 armed it. **A field that DRIVES its own conditional block lost its BASE rules until #B229:** `forEachField`'s per-field tail read `if (isInCase || caseName == field) continue;`, and `caseName` is assigned inside the `_case_` scan loop that re-runs in full on EVERY field iteration, so it always held the LAST scanned `_case_` key's driver name — when the iterated field WAS that driver the `continue` skipped the rest of the iteration, base-rule check included. A rule shape `{ "group": { "isRequired": true }, "_case_group": { "conditions": [...] } }` therefore never adjudicated `group`'s own `isRequired` on the bind pass, the live-check global pass OR the submit pass: the form never gated and an empty submit went out with zero client-side validation — the silent-submit class above, resurfacing for the self-driving shape and structurally DOWNSTREAM of the collection fix (the group IS collected as `''`; only adjudication was missing). The tail is now split: `isInCase` keeps its own `continue` (it is dead code — never assigned truthy — and is preserved as such), and the `caseName == field` arm runs the base-rule check before continuing, restoring the driver's collected value around the call (the check deletes the field from the object it is handed, and that object is where the scan block re-reads the case VALUE on every later field iteration; a deleted entry re-seeds from the DOM, which for a radio group is the FIRST member's value regardless of `.checked`). Which conditions apply is unchanged — the direct-case block is never entered for a self-driving case, pre- or post-fix (measured) — and the fix is order-independent: a driver declared BEFORE another `_case_` block was already adjudicated (the tail's comparison never matched it), so the post-fix union is every driver carrying base rules. Client-only: the server form-body path throws earlier on any `_case_`-bearing rule set (conditional rules are unsupported there). Enforcement-tightening: a form built on this shape starts gating where it silently submitted. Known interplay, pre-existing: on a rule set with NO `$` tokens, a pick whose value matches a `_case_` condition lets the case machinery PERSISTENTLY replace injected fields' rules in the live store (the site-B replacement), so a later `reBind()` can re-arm the gate from the mutated store — `$`-bearing rule sets are immune (the dynamised-rules path clones). **A conditional driver's collected VALUE survives every full-form pass since #B230:** the base-rule check deletes each adjudicated field from the object it is handed, and until #B230 only the last-declared driver's entry was restored (the #B229 arm above) — any OTHER field that both carries base rules and drives a `_case_` lost its stored case value the moment its own rules were adjudicated, so later field iterations re-read it from the DOM (a radio group's FIRST member regardless of `.checked`) and matched conditions against a value the user never picked — spuriously requiring the wrong flow's fields (a correctly-completed picked flow could not submit), or with excluding condition rules under-validating the picked flow — while the driver's own direct-case block read `undefined` in the same pass and matched nothing. The entry is now backed up and restored around the base-rule check for any field driving a `_case_` in the live rules OR the pass-entry rule clone; the union matters because inside a direct-case recursion the pass's rule set is the condition's own rules, which carry no `_case_` keys, so the live-rules test alone is blind there. Non-driver fields keep the deletion untouched (the condition pull-in gate, the direct-case exclude injection and the async-`query` re-validation input all read those absences today), and a driver with no rules of its own is byte-identical — including the legitimate first-scan DOM seed for rule-less unchecked groups, which is preserved. **`setFlash` `[null, "message"]` works client-side since #B226 (the form the reference documents):** it previously lost its custom message in the browser ONLY — `lib/merge` classified a `null` array element as an object (`typeof null`) and dropped it on every no-override merge, and the client rules path re-merges the whispered rules (the `data-gina-form-rule` bind merge, the `gina.hasValidator` instance re-merge, the `_case_` merges), so the engine received a one-element array, bound the message to the ignored first `regex` argument, and rendered the built-in label; `["", "message"]` always survived (empty strings, `false` and `0` are primitives — `null` was the only casualty), and the server was unaffected (it reads the boot-loaded rules without those hops). The fix is in `lib/merge` itself, so no-override merges now preserve `null` array elements as VALUES framework-wide (and the index-merge branch stops manufacturing `{}` from a `null` source element) — a merge consumer relying on the silent compaction sees the `null` slots preserved. **Bracket-notation and nested-authored rule KEYS enforce on the SERVER form-body path since #B241:** the rule parser canonicalizes every rule key to a dotted path (`account[username]` becomes `account.username`; a nested rule tree flattens to its dotted leaves) while the server's fields map kept the RAW posted keys, so such rules never joined — the field was silently skipped with no warning, fail-open for every rule-keyed directive alike: checks (`isRequired`, `isEmail`, ...), the `exclude` drop, and value transforms — on BOTH production wire shapes (flat bracket keys: the client posts its name-keyed data as JSON and the JSON body path deliberately does no bracket expansion; and nested objects: the multipart and urlencoded parsers expand bracket names). The server now synthesizes dotted-canon field aliases ALONGSIDE the raw keys (originals kept, so `$name` cross-field tokens keep resolving off the raw posted names, and an all-flat payload synthesizes nothing — byte-identical behaviour), then folds alias outcomes back at egress: error keys return under the DOM-name bracket form the client renders against, and the validated data output keeps its materialized shape with exclusions and transforms applied (a parent object emptied by an exclusion is pruned along that alias's path only — a posted empty object survives). The client join was always bracket-on-both-sides (a named rule set passes through with its authored keys; nested-authored sets are reconstructed to bracket names at bind time), so this brings the server to parity — quirks included: a caller that posts the dotted key form keeps its own addressing, and the no-rules path still returns the payload verbatim. Behaviour change by design: a bracket-keyed or nested-authored rule that never fired before now enforces — anything relying on the old silent skip starts rejecting or dropping those fields.
895
+ 209. **FormValidator — engine disambiguation, string inputs, a11y reflection, and live-check message visibility (consolidates former #42/#130/#150/#186/#197).** The live form/data rule engine is `core/plugins/lib/validator/src/form-validator.js` (single source, `isGFFCtx`-branched: runs server-side via `backendInit` AND compiled into the browser bundle) with the client orchestration in `validator/src/main.js` — `framework/v*/lib/validator.js` is a DEAD standalone fluent validator with overlapping `is*` rule names; never edit it for form-rule work (tell them apart fast: the live engine's `isRequired` rejects whitespace-only input, the dead one passes it). Rule bodies must handle STRING inputs — the two DECLARATIVE contexts feed strings (`.value` + urlencoded bodies) — so a typed/numeric rule coerces or parses explicit components, never assumes a typed JS value; but "always a string" is NOT true of every path, and reading it that way shipped #B198 (see below): a JSON request body keeps real Numbers (`JSON.parse` → `req.body`/`req.post`, which the `validator::{}` routing path MERGES into the validated data before spreading array bounds through `apply()`), and `toInteger` leaves `Math.round()`'s real Number on `this.value`, so a `toInteger` → `is*` chain hands the next rule a Number even in the browser — a rule must therefore be correct for a typed value too, not merely tolerant of strings: `isFloat` coerces via `Number()` (#B46); `isDate` builds from explicit mask components + a round-trip check so non-ISO slash masks aren't US-misparsed and impossible dates still reject (#B47); `isDate` returns the FIELD again on its valid path (#B48, 0.5.4 — parsed `Date` preserved on the field's `.value`, the `isDate(mask).format(...)` idiom unchanged), so rule chaining works. An empty value is adjudicated by `isRequired` ALONE (#B78): the per-rule empty-bypass became unconditional on empty (`if (this.value == '')`, its old `!errors['isRequired']` gate dropped) for `isEmail`/`isJsonWebToken`/`isFloat`/`isInList` — each regating `this.valid = isValid && !errors['isRequired']` — and `isString` keeps the field invalid without recording a second message, so a required-empty field shows ONE message (`is required`) not two, optional empty fields still pass, a filled-but-invalid value still reports its own error, and custom `is` was deliberately excluded by #B78 and re-declined by #B82 — an exclusion REVERSED by #B233 (2026-08-03, unreleased, `307721f2`): `is` now carries the same canonical strict bypass (`if ( this.value === '' ) { isValid = true; }`) and the same regate, taking the Shape-A population from four rules to FIVE, so a required+EMPTY field carrying an `is` condition records `isRequired` ALONE instead of also collecting a second `Condition not satisfied`. `isBoolean` joined the same contract at #B235 (2026-08-03, unreleased, `aa1c2035`), taking that population to SIX: its pre-switch rescue `errors['isRequired'] && this.value == false` was LOOSE (`'' == false`), so a required+EMPTY boolean field LOST its isRequired error and reported `Must be a valid boolean` instead of `Cannot be left empty`; the rule now takes the canonical strict `=== ''` self-pass, and the rescue moves AFTER the accept-set switch gated on the value having been ACCEPTED (`val !== null`) — which keeps the documented unchecked-but-required-toggle case working, since a recognized `false`/`0` is a present answer, while emptiness returns to `isRequired` alone. Paired in the same commit with #B236, the SERVER-side half: the plugin's `getCastedValue` funneled EVERY value on an isBoolean-ruled field through `/^true$/i ? true : false` BEFORE the engine ran (client AND server — `validate` calls `formatFields` unconditionally), so on the server auto path junk validated CLEAN and PERSISTED as `false` — `nope`, the HTML checkbox default `on` (a CHECKED box storing UNchecked), the strings `1`/`0`, `TRUE`/`True` — and the NUMBER 1 stored `false` where the engine reads it as `true`. The pre-cast now survives ONLY in dynamised-rules mode, where a referenced boolean field must splice into a stringified `is` condition as an unquoted operand (measured NECESSARY: deleting it outright breaks a server `$flag === true` condition); the ENGINE is the single adjudicator on every surface, which is what the routing `validator::` surface always enforced and what the published reference already promised. Disclosed both directions: values that silently stored `false` now ERROR, the number 1 flips its stored value `false`→`true` on a verdict that was already valid, an optional blank boolean field now PASSES instead of erroring, and a required blank field's message changes from isBoolean to isRequired. A sibling server-path crash in the same plugin is fixed by #B234 (2026-08-03, unreleased, `7c56565d`): `getDynamisedRules` substitutes in two passes, and the SECOND is a DOM fallback re-deriving each splice value from the live element (`$fields[...].value`) — which `backendInit` calls with `$fields = null`, so it threw `TypeError: Cannot read properties of null` on its FIRST iteration for ANY `$` surviving pass 1: a regex end-anchor in an `is` condition, a `$` inside a human-readable message string, or a `$` in any array-rule element after the first. Plain cross-field `$peer === $me` never crashed, because pass 1 consumes tokens that NAME fields. The loop is now gated `$fields && ...`, joining the #B127 precedent one function later; `validate`'s same-text gate is deliberately left UNGUARDED, being reachable only with a live DOM. Residual, disclosed — and since FIXED (#B239): a `$` token in an ARRAY rule's FIRST argument that names no field (`isInList: ['$100']`) threw one site later at `checkFieldAgainstRules`' `d[<token>].value` — NOT DOM-dependent, so it reached the client too. The substitution is now gated on the token resolving to a REAL field (an existing `d` key with a defined `.value` — two clauses, both load-bearing: an engine-METHOD-name collision like `'$isValid'` resolves to a defined key with no `.value`, and pre-fix spliced the string "undefined" into the rule for a silent wrong verdict rather than a crash); anything else stays LITERAL so strict comparison applies (`'$100'` matches its own literal, rejects non-members with the rule's own error; bare-`$` and mixed elements covered). `$` is therefore the engine's RESERVED cross-field sigil: whether an authored `$` stays literal depends on a runtime field-name collision — a token naming a sibling field is consumed UPSTREAM by getDynamisedRules loop 1, substituted with quoting fit for `is`-condition splices, not array elements (`"yes"` with quotes can never match `yes`), so real cross-field refs in array-rule elements are always-invalid, fail-closed, never-worked, undocumented (the reference scopes `$name` to `is` expressions) — tracked as #B240 (demand-gated; the fix is relocating array-element substitution into checkFieldAgainstRules, whose guarded loop is deliberately preserved as the substrate). This reserved-sigil model is also the #DTO2 `$` guard's CURRENT rationale (the crash rationale is retired — deterministic literal semantics are impossible for any `$`, so toRules() refuses at boot rather than validate collision-dependently). The reversal is measured rather than re-argued: the old bypass was gated on `!errors['isRequired']` — off exactly when #B78 wants it on — beside a two-disjunct guard that was DEAD CODE (`x == '' && x != 0` has no witness), optional+empty ALREADY self-passed through the live else-if, and on required+empty the condition is VERDICT-IRRELEVANT (form validity is `getErrors().count()` and `isRequired` has already errored), so form validity and the request payload are identical in both directions and only the message list changes; the "coercion-sensitive" premise had already been retired by #B199's strict test, which leaves `0`/`false`/`null` as operands that still evaluate the condition. #B82 is neither regressed nor retired — its root `getCastedValue` quoting and its `is()` grammar guard stay necessary and reachable with a FILLED host; #B233 only closes that crash path a second time for an empty HOST, whose condition is no longer compiled at all. Same commit drops the dead `_defaultErrorLabels['isApiError']` entry (zero consult sites: the API path assigns the server's message directly and never calls `replace()`) - but a cross-field `is` (`"$a === $b"`) no longer THROWS when the referenced field is empty (#B82): the client dynamised-rules substitution (`getCastedValue` in `main.js`) now renders an empty referenced operand as a quoted `""` (it was spliced RAW, leaving a dangling `"7654321" === ` that `is()`'s binary-comparison grammar `_SCS_BINARY_RE` rejected -> an uncaught throw that aborted the whole-form validity pass and left the submit trigger ungated on an invalid form, breaking the documented `is`+`isRequired` value-confirmation pattern while the confirm field was blank), mirroring `getDynamisedRules`' own sibling substitution default (`: '\"\"'`); `null`/`undefined` stay raw (already valid operands). Hardening: `is()`'s grammar mismatch now FAILS-THE-FIELD (`console.warn`+`isValid=false`) instead of throwing, so a per-keystroke live check can never abort the gate on a residually-unparseable condition (e.g. a field literally valued `"NaN"`, which the root fix leaves raw). Browser-bundled -> prod dist rebuilt; the `#SCS1e`/`#SCS1h` eval-safety pins target the untouched `_SCS_BINARY_RE`/`_scsParseOperand`/regex-literal constructs, so the hardening flips none of them. The form's validity comes from `getErrors().count()` (the surviving `isRequired` error), never the per-field `.valid` flag (whose only error-dropping reader, `setErrors`, is dead). Length bounds are ARITY-sensitive, and the source JSDoc was WRONG about it until 0.6.3: `"isString": [N]` (same for `isInteger`/`isNumber`) supplies `minLength` ONLY — identical in effect to the scalar `N` — because the exact-length branch fires only when `minLength === maxLength`, so an exact length needs `[N, N]`; the stale comment had propagated verbatim into the published reference page, so correct BOTH surfaces when one is found. Those bounds measure the value's STRING FORM (`val.toString().length`) — until 0.6.3 `isInteger` alone measured a bare `val.length`, which is `undefined` on a real Number, so BOTH its bounds were silently inert on every numeric value: no error, no warn, field left `valid` (#B198, a fail-OPEN bypass reachable from a JSON body, a `validator::{}` requirement, or a preceding `toInteger` — the browser included). `isString` reads the same bare `val.length` at two sites and is CORRECT there because a `typeof(val) == 'string'` guard precedes it, so this class of fix is line-scoped: a whole-file replace of the bound expression hits four sites, two of which must not change. One consequence of measuring the string form, intended: a negative number counts its sign toward the length (parity with the same value arriving as a string). The zero-swallow residual #B198 initially left open is CLOSED by #B199 (0.6.3): loose `== ''` emptiness tests conflated `0`/`-0`/`false`/`[]` with the empty string at FIVE sites — the isInteger/isNumber bounds gates AND the isEmail/isJsonWebToken/isFloat empty-bypasses, where a JSON body's `{"email": 0}` validated as a correct email — all five now compare strictly, so only the literal `''` bypasses (the designed empty-is-adjudicated-by-isRequired contract, preserved byte-exactly); `isString` stays loose behind its typeof guard (operators identical for strings), `isInList` was already strict, and `isDate`'s broader `!val` swallow (a silent half-state: `valid` false, NO error recorded, so the form passes) is deliberately untouched. STILL OPEN sibling (#B200): a TRUTHY non-string in an isEmail/isJsonWebToken field (`{"email": 123}`) hits an unguarded `.toLowerCase()` and the rule driver RE-THROWS, killing the whole validation run — the falsy/truthy non-string space is partitioned between the fixed bug and this one. Custom validators (`bundle/validators/<name>/main.js`) are a BROWSER-ONLY affordance, NEVER a server-side guarantee: the server gate reads `getContext('gina').forms` while the loop it guards reads a bare `gina` that is undefined in Node, so a custom rule never attaches server-side and the engine then silently skips the unknown rule name with no warn — re-validate such constraints in the action. (Publishing that context without also fixing the loop would make EVERY validator construction throw, including for bundles shipping no custom validators.) A rule-body edit needs a prod dist rebuild AND flips the section-locked characterization tests by design. Editing trap: `form-validator.js` embeds hidden NO-BREAK SPACE bytes (U+00A0) where a normal space appears inside several `||`/ternary sequences, so a literal-space find/replace spanning one silently fails — patch such regions with a byte-scoped script over clean-ASCII substrings, not a space-spanning match. The blur-time global validation pass sets submit-button state but renders errors ONLY for the touched field — untouched invalid fields stay quiet until interacted-with or submit. Accessibility (#A11Y1): the rule-agnostic chokepoint `handleErrorsDisplay` reflects committed errors into `aria-invalid="true"` (gated on committed-not-warning; `"false"` on clear mirrors native `ValidityState` so it agrees with `:user-invalid`; hidden fields skipped), auto-wires `aria-errormessage` to a gina-owned message div UNLESS the consumer provided their own, focuses the first DOM-order invalid field on a failed submit, and announces blur-time errors via a per-form visually-hidden `aria-live="polite"` region; the per-field aria passes fire only under live-check — the always-on submit pass covers every bound form regardless. Live-region LIFECYCLE (#A11Y2): creation is split out of the announcer into `ensureA11yLiveRegion($form)` and called from `bindForm` — the chokepoint every registration path funnels through — so the region is in the a11y tree from BIND time. It previously created, inserted AND populated the region in one synchronous tick, which reaches assistive tech as a single mutation batch on a node it has never observed, so the FIRST announcement per form (the one that matters most) was the one least likely to be spoken while every later one worked. The region is a CHILD OF THE FORM on purpose: a popin renders its form inside a native `<dialog>` opened with `showModal()`, which leaves everything outside the top layer inert, so a body-level region would go unspoken for exactly the forms that live in popins — the placement is an accessibility constraint, not a convenience. The price of that choice is that a subtree replacement (`$el.innerHTML =` on a popin re-render, a nav fragment swap) destroys it, so `ensureA11yLiveRegion` is create-OR-RECOVER and also re-homes a region whose form node was replaced; any region created or re-homed at announce time is marked fresh and defers its first write one macrotask, so insertion and mutation land in different ticks — that deferral is what stops a recovery from silently repeating the defect, and it is load-bearing because a re-render does NOT always re-bind (`validateFormById` early-returns on an already-registered id, and a multi-form popin's teardown loop splices while iterating so it skips every odd-indexed form). An already-bound region writes synchronously, unchanged; while a deferred write is pending a newer error replaces the pending text so the LATEST message wins, and the timer re-enters the announcer, keeping exactly ONE `textContent` write site. Rule: a live region must be observable BEFORE it is written — separate creation from announcement, and when the same call must do both, put a tick between them. **Submit LIFECYCLE exposure (#A11Y4):** the accessibility signals for an in-flight submit hang off the request window inside `send()` — the only place reached once an XHR genuinely exists — and that placement is the fix for a timing trap, not an accident: the loading state (`data-gina-loading`) is armed at CLICK time, BEFORE validation runs, so a signal hung there would arm-then-disarm on every rejected submit and announce a spurious busy/not-busy pair. Three effects, all scoped to that window. (1) The trigger's focus is captured before gina natively disables it and restored once the request settles: a natively `disabled` control cannot hold focus, so the browser drops focus to `<body>` and re-enabling does NOT bring it back (both measured), which silently cost every keyboard submit its place. The restore is deliberately conservative — only when focus is still on `<body>` and the trigger is still in the document — so a response that opened a popin, redirected, or focused the first invalid field keeps its own focus decision; gina restores what gina took, never more. The `<a>` branch is excluded from the capture: an anchor gets `aria-disabled`, which does not blur (measured control). (2) The trigger carries `aria-busy` for the request's duration — on the TRIGGER, never the form, because the live region is a CHILD of the form and an ancestor marked busy MAY be treated as "defer announcements in this subtree", which would silence the very channel used to announce; ARIA 1.2 defines no normative behaviour here, so that is a cheap hedge, and the same reading is why `aria-busy` can never substitute for an announcement (it produces none of its own). ⚠️ **Do NOT restate this as "assistive tech commonly defers"** — that wording shipped once and was WRONG: measured 2026-08-05 on VoiceOver/Chrome, an announcement made from inside an `aria-busy="true"` ancestor was spoken normally, so at least that pairing does not defer. The placement costs nothing and still guards ATs that might, but it is a precaution, never a claim about implementations. (3) The start is announced ONCE through the #A11Y2 region; completion announces NOTHING by design, because an errored response is already announced field-by-field by `handleErrorsDisplay` and a second status write over the same polite region in the same beat can truncate it. Announced strings are the framework's own, so they resolve through `gina.config.a11y` with English defaults (e.g. `{ submitting: 'Envoi…' }`) — deliberately separate from `setErrorLabels`, which is keyed by RULE name and owns rule messages. Rule: put a state signal where the state actually begins; a signal armed on intent rather than on the operation announces work that may never happen. **Error-association integrity (#A11Y5):** three fixes sharing one theme — an ARIA assertion is only worth what its target is worth. (a) Hiding the message div used gina's `.hidden` helper (`display: none !important`), which removes it from the accessibility tree entirely — fine for a soft warning, wrong at the TWO hide paths that coexist with an asserted `aria-invalid` (`refreshWarning`'s focus-driven hide, and the refresh re-create), where the field was announced invalid while `aria-errormessage` pointed at an unreachable target. Both now CLIP instead: inline declarations that hide visually but keep the node in the tree, with `display` carrying `!important` because that is what outranks the class's own `!important` (measured — a plain inline `display:block` loses to it); the class is deliberately left in place so consumer CSS keyed on `.hidden` keeps matching. (b) A polite region is announced on CHANGE, so re-writing byte-identical text is commonly not spoken — which is precisely the repeat-error case (blur a field that still fails the same rule). A trailing no-break space now makes the content differ without altering what is read out, and it self-cancels on the next write. NOTE this half is source-derived: the string demonstrably changes, but no assistive-technology pass has confirmed the re-announcement. ⚠️ The same caveat NO LONGER applies to #A11Y2's first-announcement fix — **V1 was CONFIRMED 2026-08-05 on VoiceOver/Chrome** by an A/B whose two arms differ only by a `setTimeout(0)`: the same-tick create-and-write was NOT spoken, the deferred write WAS. So the deferral is load-bearing in practice, not merely defensible in theory; the region-lifecycle discipline above is measured, not inferred. (c) `focusFirstInvalidField` (and its deliberately-duplicated inline twin) gated focusability on `typeof $field.focus == 'function'`, which is TRUE for every HTMLElement — a custom element with neither `tabindex` nor `delegatesFocus` passes it and its `focus()` is a silent no-op (measured), so a failed submit whose first invalid control was such a host focused nothing AND stopped searching. Both loops now confirm `document.activeElement` actually moved before stopping, which is also what makes the JSDoc's "skips unfocusable controls" claim true rather than aspirational. Rule: a capability probe (`typeof x.focus == 'function'`, `'foo' in el`) tests the API's PRESENCE, never its EFFECT — when the effect is what matters, assert the resulting state. Error-MESSAGE visibility has THREE write paths (create / refreshWarning's un-hide / the refresh re-create) and the re-create runs LAST in the live-check pass, so it owns the steady state: it is focus-aware — message hidden while the edited field is the active element, revealed on blur (soft warning border while typing). Rule: when an element is written by multiple paths in a single validation pass, guard the LAST writer — an earlier-writer fix is silently overridden. The invalid submit trigger is marked `aria-disabled="true"` + the class `gina-form-submit-disabled`, NEVER native `disabled` (#B76 — a natively-disabled button emits no click, so the validate-render-focus guard could never run); `isValid()` is the real send gate, and **consumers style that state — the framework ships no button CSS**. Form-associated custom elements (FACEs) participate in binding + live-check (#CC2 — hyphenated members of `form.elements`; their own `.value` accessor is honoured, live-check rides the composed bubbling `change`; author contract: `static formAssociated`, a `name` attribute, a `.value` getter, composed `change` on commit). **Radio-group collection (#B221):** an unchecked non-boolean radio group whose rule declares a truthy `isRequired` is collected as an EMPTY value by BOTH collectors (`getFormValidationInfos` + the native-submit inline copy) so `isRequired` adjudicates it via the standard emptiness test — pre-fix no collection arm admitted the shape (each required `.checked`, a `true|false`-shaped value, or an `isBoolean` rule), the DOM handle was held in `$fields` but the VALUE never entered `fields`, so no rule ran against the group and a radio-group-only form short-circuited BOTH submit guards (field count 0 reads as nothing-to-validate → synthetic `isValid() === true`) and submitted its XHR with zero client-side validation. Other unchecked groups stay absent-when-unchecked (native parity: no rule / `isRequired: false` unchanged; `isBoolean`-declared groups keep the force-false arm), checked members post exactly as before, and on the auto path a required-empty form is invalid and never sends — so the wire only changes for the newly-gated shape. Enforcement-tightening: forms that silently submitted with nothing picked now gate on the pick (trigger `aria-disabled` at bind under default-on live-check, message on submit attempt, re-enabled after picking). **The re-enable is real only since #B228:** the radio live-check listener was registered under a `changed.<id>` event name nothing dispatches on a user pick — the form-level click proxy short-circuits into the radio state updater (which never dispatches any gina event), and the change proxy dispatches ONLY names present in the event registry, which radios never registered (checkboxes have that registration via their state-updater relay; radios' equivalent relay is keyed on the bare element id, which nothing triggers) — so the whole-form silent pass never re-ran after a pick and the trigger kept its bind-time disabled state indefinitely, while submit-time validation (a separate call chain) accepted the checked group and let the click-guard send: flows completed, only the trigger state was wrong (announced disabled to assistive tech; automation actionability checks refuse `aria-disabled`). Radios now ALSO register the proxy-dispatched `change.<id>` name alongside `changed.<id>` — the handler's radio arm accepted `change.`-typed events all along, so one registration line closes the loop: mouse, label and keyboard picks all re-run the field + whole-form passes (single delivery per pick — native `change` fires only on real state changes; the legacy `changed.<id>` name stays registered for the relay/programmatic path; checkboxes byte-identical). Latent since the live-check's introduction, invisible until #B221 armed it. **A field that DRIVES its own conditional block lost its BASE rules until #B229:** `forEachField`'s per-field tail read `if (isInCase || caseName == field) continue;`, and `caseName` is assigned inside the `_case_` scan loop that re-runs in full on EVERY field iteration, so it always held the LAST scanned `_case_` key's driver name — when the iterated field WAS that driver the `continue` skipped the rest of the iteration, base-rule check included. A rule shape `{ "group": { "isRequired": true }, "_case_group": { "conditions": [...] } }` therefore never adjudicated `group`'s own `isRequired` on the bind pass, the live-check global pass OR the submit pass: the form never gated and an empty submit went out with zero client-side validation — the silent-submit class above, resurfacing for the self-driving shape and structurally DOWNSTREAM of the collection fix (the group IS collected as `''`; only adjudication was missing). The tail is now split: `isInCase` keeps its own `continue` (it is dead code — never assigned truthy — and is preserved as such), and the `caseName == field` arm runs the base-rule check before continuing, restoring the driver's collected value around the call (the check deletes the field from the object it is handed, and that object is where the scan block re-reads the case VALUE on every later field iteration; a deleted entry re-seeds from the DOM, which for a radio group is the FIRST member's value regardless of `.checked`). Which conditions apply is unchanged — the direct-case block is never entered for a self-driving case, pre- or post-fix (measured) — and the fix is order-independent: a driver declared BEFORE another `_case_` block was already adjudicated (the tail's comparison never matched it), so the post-fix union is every driver carrying base rules. Client-only: the server form-body path throws earlier on any `_case_`-bearing rule set (conditional rules are unsupported there). Enforcement-tightening: a form built on this shape starts gating where it silently submitted. Known interplay, pre-existing: on a rule set with NO `$` tokens, a pick whose value matches a `_case_` condition lets the case machinery PERSISTENTLY replace injected fields' rules in the live store (the site-B replacement), so a later `reBind()` can re-arm the gate from the mutated store — `$`-bearing rule sets are immune (the dynamised-rules path clones). **A conditional driver's collected VALUE survives every full-form pass since #B230:** the base-rule check deletes each adjudicated field from the object it is handed, and until #B230 only the last-declared driver's entry was restored (the #B229 arm above) — any OTHER field that both carries base rules and drives a `_case_` lost its stored case value the moment its own rules were adjudicated, so later field iterations re-read it from the DOM (a radio group's FIRST member regardless of `.checked`) and matched conditions against a value the user never picked — spuriously requiring the wrong flow's fields (a correctly-completed picked flow could not submit), or with excluding condition rules under-validating the picked flow — while the driver's own direct-case block read `undefined` in the same pass and matched nothing. The entry is now backed up and restored around the base-rule check for any field driving a `_case_` in the live rules OR the pass-entry rule clone; the union matters because inside a direct-case recursion the pass's rule set is the condition's own rules, which carry no `_case_` keys, so the live-rules test alone is blind there. Non-driver fields keep the deletion untouched (the condition pull-in gate, the direct-case exclude injection and the async-`query` re-validation input all read those absences today), and a driver with no rules of its own is byte-identical — including the legitimate first-scan DOM seed for rule-less unchecked groups, which is preserved. **`setFlash` `[null, "message"]` works client-side since #B226 (the form the reference documents):** it previously lost its custom message in the browser ONLY — `lib/merge` classified a `null` array element as an object (`typeof null`) and dropped it on every no-override merge, and the client rules path re-merges the whispered rules (the `data-gina-form-rule` bind merge, the `gina.hasValidator` instance re-merge, the `_case_` merges), so the engine received a one-element array, bound the message to the ignored first `regex` argument, and rendered the built-in label; `["", "message"]` always survived (empty strings, `false` and `0` are primitives — `null` was the only casualty), and the server was unaffected (it reads the boot-loaded rules without those hops). The fix is in `lib/merge` itself, so no-override merges now preserve `null` array elements as VALUES framework-wide (and the index-merge branch stops manufacturing `{}` from a `null` source element) — a merge consumer relying on the silent compaction sees the `null` slots preserved. **Bracket-notation and nested-authored rule KEYS enforce on the SERVER form-body path since #B241:** the rule parser canonicalizes every rule key to a dotted path (`account[username]` becomes `account.username`; a nested rule tree flattens to its dotted leaves) while the server's fields map kept the RAW posted keys, so such rules never joined — the field was silently skipped with no warning, fail-open for every rule-keyed directive alike: checks (`isRequired`, `isEmail`, ...), the `exclude` drop, and value transforms — on BOTH production wire shapes (flat bracket keys: the client posts its name-keyed data as JSON and the JSON body path deliberately does no bracket expansion; and nested objects: the multipart and urlencoded parsers expand bracket names). The server now synthesizes dotted-canon field aliases ALONGSIDE the raw keys (originals kept, so `$name` cross-field tokens keep resolving off the raw posted names, and an all-flat payload synthesizes nothing — byte-identical behaviour), then folds alias outcomes back at egress: error keys return under the DOM-name bracket form the client renders against, and the validated data output keeps its materialized shape with exclusions and transforms applied (a parent object emptied by an exclusion is pruned along that alias's path only — a posted empty object survives). The client join was always bracket-on-both-sides (a named rule set passes through with its authored keys; nested-authored sets are reconstructed to bracket names at bind time), so this brings the server to parity — quirks included: a caller that posts the dotted key form keeps its own addressing, and the no-rules path still returns the payload verbatim. Behaviour change by design: a bracket-keyed or nested-authored rule that never fired before now enforces — anything relying on the old silent skip starts rejecting or dropping those fields. **Upload previews carry a text alternative (#A11Y7/U1, unreleased).** The staged-upload client layer builds its preview `<img>` in two MUTUALLY EXCLUSIVE branches of `onUpload` — one for a file with no server-side `preview` object, one for a returned preview variant — and neither set `alt` at all (not even `alt=""`), so assistive tech fell back to reading the temp URI aloud, once per staged file (WCAG 1.1.1). Both now set `alt` from `files[f].originalFilename`: the name the USER chose, deliberately NOT the sibling `files[f][key].originalFilename` of the preview variant, which is a server-generated artefact that means nothing to the person listening — the same distinction the adjacent `data-upload-original-filename` / `data-upload-preview-original-filename` pair already encodes. The fallback is `''` (a properly ignored image) rather than letting a missing name be spoken as a placeholder. The preview is INFORMATIVE, not decorative: it is the only signal telling a user which file is staged, and everything else in the upload layer is still silent — progress is attribute+`textContent` with no `role="progressbar"`/`aria-value*`, upload errors are an `innerHTML` write with no `role="alert"` that never calls the polite region the plugin already owns, and the reset control is an `<a href="#">` (U2/U3/U5, tracked in the accessibility audit, not fixed here). Maintainer gotcha: the two branches are per-file exclusive, so a one-site fix silently misses every upload whose server returns a preview object — fix both or neither. **A submit trigger marked disabled is now genuinely NOT OPERABLE (#B246, unreleased).** `updateSubmitTriggerState()` marks an invalid form's trigger with `aria-disabled="true"` + the `gina-form-submit-disabled` class and deliberately never native-`disabled` (a natively-disabled button emits no click at all, so nothing could tell the user WHY it is dead) — but NOTHING READ that marker, so a click ran the entire submit cycle (collect → validate → `validate.<id>`) and only the `isValid()` gate stopped the send: the trigger was inert in appearance ONLY, and a control announced as disabled to assistive tech stayed fully operable, which the `aria-disabled` contract forbids. `clickProxyHandler` now intercepts a disabled trigger BEFORE the `submit.<id>` dispatch — so `bindSubmitEl`'s handler never runs, `isSubmitting` is never latched, and no send path is reachable by construction — and answers the click with a display-only `revealValidationState()` pass that renders every invalid field, focuses the first, and re-syncs the trigger state so a STALE disabled marker on a form that has since become valid heals itself. The predicate `isTriggerDisabled()` reads the CLICKED element rather than `$formInstance.submitTrigger` (a form may carry several submit buttons while only one registers) and mirrors the popin plugin's existing trigger gate, so both subsystems agree on what "disabled" means. Gotcha for maintainers: `focusFirstInvalidField()` deliberately DUPLICATES the inline focus loop inside the `validate.<id>` guard — that inline shape (`_a11yErrs` / `_aField`) is locked by source pins, so collapsing the two would break tests unrelated to this fix; keep them in step by hand if the focus rule ever changes. **Staged uploads speak (#A11Y7/U2+U3+U5, unreleased).** Three defects in one client layer, fixed together because they share one plumbing change. (a) `updateUploadProgressIndicator` wrote `textContent` plus two `data-gina-upload-progress*` attributes and NO ARIA, so any indicator that is not a native `<progress>` was unlabelled text; it now carries `role="progressbar"` + `aria-valuemin="0"` / `aria-valuemax="100"` / `aria-valuenow`, with `aria-valuenow` tracking the percent attribute EXACTLY — absent while `preparing`/`indeterminate` and on `error`, because an absent value is how a progressbar signals "unknown" while a stale or zeroed one reads as real, stalled progress. `reset` strips the ARIA too (it strips everything this layer ever set); `processing` advances the state only, preserving the last value so a determinate bar stays full through the server-side window. A native `<progress>` is deliberately untouched — it already exposes all of this, and an explicit role risks overriding the implicit one. (b) The TRANSITIONS are announced through the polite region `bindForm` already stands up: `uploadStarted` at the selection kickoff — deliberately NOT gated on the indicator being present, since opt-in-by-presence is right for a visual and wrong for an announcement — and `uploadComplete` in the success branch only. Per-tick progress is NEVER announced: `aria-live="polite"` coalesces but does not throttle, so one announcement per `onprogress` event would bury every other message on the page. There is deliberately NO generic `uploadError` key, because the error path announces the server's own message and a generic second write in the same beat would clobber it (the region is latest-wins — the same reasoning that keeps `releaseSubmitA11y` silent). (c) The upload error container is written via `innerHTML` while `display:none` and revealed only by a fade-in — exactly where a `role="alert"` on it is unreliable — so the error routes through that same region, announcing `$error.textContent` (the RENDERED text: a message can carry markup, and the url-action checker substitutes `<br>`). Note the container being consumer-owned is NOT the discriminator: the progress indicator is equally consumer-supplied and gina already writes attributes into both; the hidden-then-revealed shape is what rules out `alert`. (d) The generated reset control was `<a href="#">Reset</a>` — announced as a link, not activatable with Space, and identical across every staged file. It now carries `role="button"` plus an `aria-label` of `<visible label> <filename>`, the visible label kept as a PREFIX so the accessible name still contains it (WCAG 2.5.3 Label in Name) and reusing the consumer's own `data-gina-form-upload-reset-label` so it is translated wherever that already is. The role is stamped ONLY on an anchor (strict `/^a$/i`, unlike the loose `/a/i` guarding `href` a few lines above, which also matches TEXTAREA/CANVAS/LABEL), so a consumer supplying their own `<button>` keeps its implicit role. A `role="button"` that cannot be operated by Space would be worse than no role at all, so the binder adds a `keydown` handler mapping Space to the removal — anchors only, since a native button already fires click on Space and would otherwise run the removal twice. (e) That control is REMOVED from the DOM while holding focus (`$resetLink.remove()`, which its own comment requires be last so the click listener dies with the node). The finding described this as a `display:none`; both happen, and the removal is the decisive one — a fix aimed only at the hide would not have worked. Focus now moves to the file input BEFORE the removal, guarded on the control actually holding focus (a programmatic reset must not steal focus from elsewhere) and CONFIRMED afterwards rather than assumed, since `typeof focus == 'function'` is true for every HTMLElement and `focus()` is a silent no-op on one that cannot take it. The removal is then announced (`fileRemoved`, default `'%s removed'`) using a FUNCTION replacer — never a string — because a file name may contain `$` and a string replacement would expand `$&`/`$1` patterns inside it. All three new strings live in `A11Y_LABELS`, so `gina.config.a11y` overrides them. **Maintainer gotcha worth the line:** `test/core/validator-upload-reset-delete.test.js`'s `fnSlice` takes a FIXED 12000-char window from its declaration anchor, and this fix grew `onUploadResetOrDelete` by ~2.6k chars — pushing the `#R8` strip and the callback dispatch outside it and reddening four pins whose code was present and correctly ordered the whole time. Widened to 16000 (the function ends ~14.2k past its anchor), but it is still a fixed window and will bite again; the durable fix is to brace-walk to the real function end, as the sibling `validator-upload-progress.test.js` already does. **Not verified against assistive technology** — source-derived plus a served-bytes check, like the rest of this arc.
896
896
 
897
897
  210. **Multipart upload config is ENFORCED — groups, destination, limits, text-field capture + caps, terminal states (consolidates former #187/#188/#228/#230's server half; #B49/#B50/#B51/#B92-adjacent/#B93/#B97).** Every uploaded file must map to a CONFIGURED upload group: the resolved group (no/empty → the default `untagged`) must exist in `settings.json upload.groups` or the request is rejected 400 BEFORE the temp file is created, and `untagged` obeys its own config like any named group (pre-#B50 the checks ran only for defined non-`untagged` groups — an allow-list bypass; the shipped `untagged` default is `allowedExtensions: '*'` + `isMultipleAllowed: true`, so configure `untagged` restrictively or a client can route around a named group's allow-list). Destination + limits honoured: files stream to `uploadDir || tmpPath || os.tmpdir()` with a per-group `path` override and mkdir-if-missing before `createWriteStream` (the mkdir itself crash-guarded → 500, #B145); `maxFields` caps the per-request file COUNT (400 past the cap; 0/unset disables); `maxFieldsSize` parses its unit suffix (B/KB/MB/GB; bare number = MB) as the whole-body cap (431). **Multipart TEXT fields are captured** — a real busboy `'field'` listener (busboy silently SKIPS all non-file parts when none is registered) exposes them on `req.body` + `req[method]` (POST/PUT/PATCH only), values VERBATIM (no url-decode, no `"true"/"false"/"on"/"null"` coercion — the JSON body contract, deliberately: otherwise the same client `send(fd)` call would change value TYPES with file presence), bracket-notation names nested through the urlencoded path's own layer, duplicate plain names last-wins; caps `upload.maxTextFields` (default 1000) + `upload.maxTextFieldSize` (default 1MB, unit-aware, explicit 0 = no limit) answer 400 on breach instead of busboy's silent skip/truncation. **A multipart request with no successfully-parsed file part reaches a TERMINAL state:** fields-only → the request resumes; malformed/empty → 400 via `busboy.on('error')` with a double-response guard (busboy terminates in exactly `finish` XOR `error`; pre-#B93/#B97 both were unauthenticated pre-routing DoS holes — an eternal hang, and an uncaughtException → SIGTERM bundle kill). Rules: a per-group restriction is only a control if the UNCONFIGURED/default case is denied or constrained, never waved through; a documented config key is only real if a code path reads it — grep the consumer before assuming a setting works; any stream/parser whose SUCCESS path drives a request's continuation must ALSO handle its error/empty terminals. Server-side only. Tests: `test/core/upload-groups.test.js` + `upload-config.test.js` + `multipart-nonfile-terminal.test.js` + `multipart-field-capture.test.js` (+ the client `send(FormData)` half: `validator-send-formdata-multipart-fields.test.js`).
898
898
 
@@ -900,7 +900,7 @@ Dev-mode query instrumentation captures every database query tied to the current
900
900
 
901
901
  212. **Structured (JSON) logging — `GINA_LOG_FORMAT=json` + per-request `requestId`/`durationMs` (consolidates former #152/#155; #M12a/#M12b).** The logger resolves its render format ONCE at init into `opt.format` (precedence `GINA_LOG_FORMAT=json|text` > `GINA_LOG_STDOUT` truthy ⇒ json > `text` default) BEFORE containers are cloned; a JSON line is `{ts, level, bundle, message, group, msg}` — `bundle`/`message` canonical, `group`/`msg` retained as additive back-compat aliases — and the raw `console.log` path honours the format too (otherwise JSON mode would interleave plain lines and break a collector); the `text` default keeps container logs byte-identical. Per-request `requestId` + `durationMs` ride JSON logs via a NARROW `AsyncLocalStorage` (`process.gina._reqALS`, parked on `process.gina` so it survives dev require-cache busting), gated on JSON logging ONLY (text renders no id field, so the ALS would be pure overhead — the text path stays byte-identical/zero-cost): the id resolver honours a SANITISED inbound `X-Request-Id` (`/^[\w.\-]{1,128}$/`, regenerate-on-violation kills log forging) else `crypto.randomUUID()`; the `.run({requestId, startMs})` wrap sits at `handle()` — NOT the request-entry handler — because the `request.on('end')` boundary between them loses async context while `handle()`'s awaits preserve it (the original body became `_handleDispatch`; `handle` is a thin wrapper); HTTP/2 gets per-stream scoping free via Node's per-stream compat `'request'` event, NOT `session.on('stream')`; the JSON-assembly sites read `getStore()` and add `requestId` + per-line `durationMs`, gracefully absent for CLI/boot/off-request logs. `.run()`, never `enterWith()` (enterWith bleeds sideways across siblings). **MS1 (2026-07-24, unreleased):** the same id is now PROPAGATED beyond the process — every `self.query()` outbound path (the file-download proxy + both the HTTP/1 and HTTP/2 inter-bundle clients) forwards it as `x-request-id` (sourced from the resolved `req._ginaReqId`, never a raw inbound header; a caller-set value always wins), and `server.js onRequest` ECHOES `X-Request-Id` back on every response — ungated / independent of `GINA_LOG_FORMAT` (the id is always-on even when the JSON-log field isn't) and guarded against an already-sent response — so one logical request stays correlatable as it fans out across bundles and a caller/LB/APM reads the id off the wire. `controller.js`/`server.js` are server-side → no dist rebuild; restart to apply. Tests: `test/core/request-id-propagation.test.js`.
902
902
 
903
- 213. **Client-plugin discipline — CSP-safe listeners, native-dialog parity, preload coalescing (consolidates former #149/#159/#200).** (1) Client-bundle plugins must NOT inject inline event-handler attributes (`setAttribute('onclick', …)`, `el.onX = …`) — they trip CSP `script-src-attr` under nonce-based policies; suppress defaults with `addEventListener('click', e => e.preventDefault())`, and keep it `preventDefault`-ONLY (no stopPropagation) when the element participates in event delegation other handlers depend on — a `preventDefault`-only listener still sets `event.defaultPrevented` exactly as the inline handler did. (2) Dialog popins open as native modals (`$el.showModal()`) in EVERY env — the dev-only non-modal downgrade + manual overlay are gone for dialog mode (native `::backdrop`); a consumer that PRE-OPENS the dialog (skeleton loading) must also use `showModal()` or it is born non-modal with nothing positioning it; the opt-in `preOpen`/`loadingShell` skeleton is idempotent via `hasAttribute('open')` — NOT getAttribute truthiness, because `showModal()` sets `open` to the EMPTY string; a native UA close (Escape, `<form method="dialog">`) fires the element's `close` event WITHOUT the plugin's own close path, so a once-per-element `close` listener (the event is `close`, not `cancel` — `cancel` is Escape-only) routes it through the plugin close, keeping open-state/listeners/toolbar consistent — a UA-closed shared popin otherwise re-opens "one render behind" (#B58). (3) A popin/dialog click fired TWO identical GETs because the hover/`focusin` preload — `focusin` is part of the click gesture — was never reused: an in-flight preload registry lets the click-time consume ADOPT the in-flight fetch (waiter woken with the body, caller's own load on failure) instead of fetching again, and the legacy click path consumes preloads at all now (#B54); only the initial GET is ever preloaded. The consume path bypassing the full load tail (redirect/JSON handling) was ASSUMED acceptable — but a hover/focus-WARMED trigger whose GET returns a redirect/JSON response (`application/json` `{isXhrRedirect,location}`) had that raw JSON blind-injected as the popin body via `applyContent` (`$el.innerHTML`), the `_self` tunnel never firing — IDENTICAL symptom to the #B77 `_self` timer race but a DIFFERENT mechanism (the consume path, regression `2c61c494` v0.5.5), so #B77 fixed only the UNWARMED click-time path and the WARMED path persisted until #B80 (2026-07-06, `37e6829c`): `preloadFetch`'s `onreadystatechange` now caches a 2xx response ONLY when its Content-Type is NOT `application/json` (mirrors `popinLoad`'s own `isJsonContent` detection), firing the in-flight waiters with `null` (→ `consumePreload`'s `onMiss` = the click-time `doLoad`) and leaving the cache empty (→ a ready consume returns false → `doLoad`), so a JSON-returning trigger falls through to the click-time `popinLoad`'s full redirect/JSON tail exactly as a non-preloaded click (one extra GET on click — the hover-warm GET is not reused; measured 2 GETs). Only a genuine HTML fragment is preloaded+injected. Browser-bundled → prod dist rebuilt; VERIFIED in a real browser (a warmed legacy `data-gina-popin-url` redirect trigger now shows the tunnel target, not the raw JSON; the decline path positively exercised via resource-timing) — jsdom is falsely green. That residual — the GET itself still fired on hover for side-effecting triggers, authenticated (same-origin cookies) and CSRF-token-less (GET) — is CLOSED by the #B91 per-trigger opt-out (2026-07-10): `data-gina-dialog-preload="false"` on the trigger (honored on legacy `data-gina-popin-url` triggers too — the gate reads the attribute off the closest-matched intent target; case-INSENSITIVE `/^false$/i` on purpose, so a templated "False" cannot fail open and fire the GET anyway — deliberately safer than data-gina-dialog-modal's case-sensitive parse) suppresses BOTH the hover warm and the focusin-on-click warm at the shared intent handler; the opted-out click loads normally at click time (undefined cache slot → consume returns false → the caller's click-time load; exactly one GET per click). Default stays preload-ON: GET is presumed safe to fire early per HTTP semantics — a side-effecting trigger declares itself (the hover-prefetch ecosystem's convention, live-verified). (4) A proxied/tunnelled redirect JSON response (`isXhrRedirect`+`location`, default `_self`) used to `$popin.load()` then arm a blind `setTimeout(50, () => !$popin.isOpen && $popin.open())`; a follow-up load slower than 50 ms opened the popin against a not-yet-injected (skeleton/empty) target → intermittent unhandled-deref crash. Removed as vestigial (v0.1.0, 2021) — the already-armed `loaded.<id>` listener opens CONTENT-FIRST (injects the body via popinBind/handleLoadedBody, THEN popinOpen), so the load alone suffices; the `_self` branch's `return;` still guards the `window.open` fall-through (non-`_self` targets only) (#B77). Event contract: `open.<id>` = a fresh open, `loaded.<id>` = reload/redirect content injected — a consumer needing "content ready on every load" must listen to BOTH. A structurally identical blind timer lived in the SIBLING validator `Validator::Popin now redirecting` path (`core/plugins/lib/validator/src/main.js`), removed by #B79 (2026-07-10): the validator DISCARDED popinLoad's return, and that return IS the `{open}` handle whose `open()` arms the content-first `loaded.<id>` listener — so the timer was papering over a race the discarded handle created (an XHR faster than 50 ms fired `loaded` into the void and the body was lost, a slower one opened an empty popin first). It now captures the handle and calls `open()` inside the same not-open gate, guarding the `undefined` popinLoad returns when the request cannot start (CORS unsupported) so a failed load never blind-opens an empty popin; and the cross-popin branch resets `isRedirecting` before closing the ORIGINAL popin, because `popinClose` early-bails on a redirecting popin — that close was a silent no-op, leaving the original open behind the new one (browser-verified: pre-fix stays open, post-fix closes). MEASURED CORRECTION: the timer was NOT load-bearing for the different-popin branch as long assumed — that branch threw first, because the published `gina.popin` WAS re-assembled with a target-wins merge on every `new Popin()`, so its `getPopinByName`/`getPopinById` stayed bound to the FIRST instance's registry and resolved ONLY the boot popin, while `$popins` + `getActivePopin` saw every popin and `gina.popin.activePopinId` never updated (so `getActivePopin()` returned null once nothing was open) — and popins registered AFTER an instance's publish (click-time in-page dialog registrations) never reached the published registry at all, with destroyed popins lingering in it. FIXED as #B90 (2026-07-10): ONE module-scoped registry (`_sharedPopins`) aliased by every instance's `$popins`; a publish-ONCE `gina.popin = instance` (the published object IS the first instance, LIVE — re-publishing was both the freeze defect and, with a shared registry, a self-merge recursion hazard: gina's merge deep-recurses plain objects and `$popin.eventData` can hold the $popin itself); and a `setActivePopinId` write-through helper at all 7 write sites keeping the published `activePopinId` truthful no matter which instance opens or closes a popin. Browser-verified both ways on the built bundle (pre-fix: accessors blind + the validator cross-popin redirect 422-throws `not found`; fixed: accessors resolve every instance's popins and a form submit redirecting into a DIFFERENT popin works end-to-end — the original closes, the target loads content-first, `activePopinId` follows). Generalises: an accessor published through a target-wins `merge()` silently keeps the first instance's closure — publish a live instance once and share module-scoped state instead of re-merging per construction; and never infer a code path is reachable from the fact that it exists. (5) EAGER content warm — `data-gina-dialog-preload="eager"` (case-insensitive) opts a trigger into a one-shot idle warm-all pass: after `window` load, on requestIdleCallback (setTimeout fallback), serialized one GET at a time, routed through the SAME shared per-trigger gate as the hover warm (`warmTrigger`: disabled skip → the `"false"` opt-out → URL-cache dedup → in-flight reserve + fetch) so the two warm paths cannot drift and whichever fires second is a no-op; skipped entirely under Save-Data; triggers injected after the pass keep the delegated hover warm; staleness matches the shipped no-TTL hover semantics — the opt-in accepts the wider warm→open window. (6) #B139 (2026-07-20, rides the next cut): the content cache no longer outlives the open it warmed — measured pre-fix it was a ONE-GENERATION-LAGGING store (every open after the first paid ~1 GET yet rendered the PREVIOUS open's fetch: the leftover entry — the #B54 in-flight adoption's repopulated body, or the around-open re-warm — blocked the fresh warm's dedup; NO invalidation path existed, close/unbind/destroy never touched the cache; reproduced back to 0.5.4, so it predates #B54 and the eager pass). Fix: close clears the popin's content-URL slot (stamped at consumePreload both branches + popinLoad) AND re-sweeps the same slot ~120ms later, because the close-time a11y focus-return and pointer re-hover fire TRUSTED synthetic intents that re-warm the slot with close-era content within 1ms of the close (measured; a raced sweep is benign by construction — at most one extra fetch, never staleness, since an adopted in-flight body still reaches its open through the waiter chain). AND `preload="false"` triggers now skip the cache READ at open on both open paths — `false` is the hard always-refetch spelling for volatile popins (its GET always happens at open, never from a same-URL sibling's warm); no new annotation vocabulary. Warmed-never-opened entries keep the page-lifetime eager semantics above; default triggers pay at most one extra idempotent GET per close (the swept synthetic warm). Content preload deliberately does NOT ride 103 Early Hints: popin responses vary on `X-Requested-With` (a non-XHR GET of a fragment layout gets the iframe-wrap variant and misses the scripts append), and a browser `Link rel=preload` fetch never carries that header — wrong-variant bytes or a double GET, with no success branch; `self.setEarlyHints()` remains the right tool for a popin's STATIC subassets (the CSS/JS it injects via getScript/getStyle). All five are browser-bundled — prod dist rebuild required; verify (2), (3), (4) and (5) with a REAL preemptive-open / preload / redirect-tunnel consumer — a minimal smoke page is falsely green.
903
+ 213. **Client-plugin discipline — CSP-safe listeners, native-dialog parity, preload coalescing (consolidates former #149/#159/#200).** (1) Client-bundle plugins must NOT inject inline event-handler attributes (`setAttribute('onclick', …)`, `el.onX = …`) — they trip CSP `script-src-attr` under nonce-based policies; suppress defaults with `addEventListener('click', e => e.preventDefault())`, and keep it `preventDefault`-ONLY (no stopPropagation) when the element participates in event delegation other handlers depend on — a `preventDefault`-only listener still sets `event.defaultPrevented` exactly as the inline handler did. (2) Dialog popins open as native modals (`$el.showModal()`) in EVERY env — the dev-only non-modal downgrade + manual overlay are gone for dialog mode (native `::backdrop`); a consumer that PRE-OPENS the dialog (skeleton loading) must also use `showModal()` or it is born non-modal with nothing positioning it; the opt-in `preOpen`/`loadingShell` skeleton is idempotent via `hasAttribute('open')` — NOT getAttribute truthiness, because `showModal()` sets `open` to the EMPTY string; a native UA close (Escape, `<form method="dialog">`) fires the element's `close` event WITHOUT the plugin's own close path, so a once-per-element `close` listener (the event is `close`, not `cancel` — `cancel` is Escape-only) routes it through the plugin close, keeping open-state/listeners/toolbar consistent — a UA-closed shared popin otherwise re-opens "one render behind" (#B58). (3) A popin/dialog click fired TWO identical GETs because the hover/`focusin` preload — `focusin` is part of the click gesture — was never reused: an in-flight preload registry lets the click-time consume ADOPT the in-flight fetch (waiter woken with the body, caller's own load on failure) instead of fetching again, and the legacy click path consumes preloads at all now (#B54); only the initial GET is ever preloaded. The consume path bypassing the full load tail (redirect/JSON handling) was ASSUMED acceptable — but a hover/focus-WARMED trigger whose GET returns a redirect/JSON response (`application/json` `{isXhrRedirect,location}`) had that raw JSON blind-injected as the popin body via `applyContent` (`$el.innerHTML`), the `_self` tunnel never firing — IDENTICAL symptom to the #B77 `_self` timer race but a DIFFERENT mechanism (the consume path, regression `2c61c494` v0.5.5), so #B77 fixed only the UNWARMED click-time path and the WARMED path persisted until #B80 (2026-07-06, `37e6829c`): `preloadFetch`'s `onreadystatechange` now caches a 2xx response ONLY when its Content-Type is NOT `application/json` (mirrors `popinLoad`'s own `isJsonContent` detection), firing the in-flight waiters with `null` (→ `consumePreload`'s `onMiss` = the click-time `doLoad`) and leaving the cache empty (→ a ready consume returns false → `doLoad`), so a JSON-returning trigger falls through to the click-time `popinLoad`'s full redirect/JSON tail exactly as a non-preloaded click (one extra GET on click — the hover-warm GET is not reused; measured 2 GETs). Only a genuine HTML fragment is preloaded+injected. Browser-bundled → prod dist rebuilt; VERIFIED in a real browser (a warmed legacy `data-gina-popin-url` redirect trigger now shows the tunnel target, not the raw JSON; the decline path positively exercised via resource-timing) — jsdom is falsely green. That residual — the GET itself still fired on hover for side-effecting triggers, authenticated (same-origin cookies) and CSRF-token-less (GET) — is CLOSED by the #B91 per-trigger opt-out (2026-07-10): `data-gina-dialog-preload="false"` on the trigger (honored on legacy `data-gina-popin-url` triggers too — the gate reads the attribute off the closest-matched intent target; case-INSENSITIVE `/^false$/i` on purpose, so a templated "False" cannot fail open and fire the GET anyway — deliberately safer than data-gina-dialog-modal's case-sensitive parse) suppresses BOTH the hover warm and the focusin-on-click warm at the shared intent handler; the opted-out click loads normally at click time (undefined cache slot → consume returns false → the caller's click-time load; exactly one GET per click). Default stays preload-ON: GET is presumed safe to fire early per HTTP semantics — a side-effecting trigger declares itself (the hover-prefetch ecosystem's convention, live-verified). (4) A proxied/tunnelled redirect JSON response (`isXhrRedirect`+`location`, default `_self`) used to `$popin.load()` then arm a blind `setTimeout(50, () => !$popin.isOpen && $popin.open())`; a follow-up load slower than 50 ms opened the popin against a not-yet-injected (skeleton/empty) target → intermittent unhandled-deref crash. Removed as vestigial (v0.1.0, 2021) — the already-armed `loaded.<id>` listener opens CONTENT-FIRST (injects the body via popinBind/handleLoadedBody, THEN popinOpen), so the load alone suffices; the `_self` branch's `return;` still guards the `window.open` fall-through (non-`_self` targets only) (#B77). Event contract: `open.<id>` = a fresh open, `loaded.<id>` = reload/redirect content injected — a consumer needing "content ready on every load" must listen to BOTH. A structurally identical blind timer lived in the SIBLING validator `Validator::Popin now redirecting` path (`core/plugins/lib/validator/src/main.js`), removed by #B79 (2026-07-10): the validator DISCARDED popinLoad's return, and that return IS the `{open}` handle whose `open()` arms the content-first `loaded.<id>` listener — so the timer was papering over a race the discarded handle created (an XHR faster than 50 ms fired `loaded` into the void and the body was lost, a slower one opened an empty popin first). It now captures the handle and calls `open()` inside the same not-open gate, guarding the `undefined` popinLoad returns when the request cannot start (CORS unsupported) so a failed load never blind-opens an empty popin; and the cross-popin branch resets `isRedirecting` before closing the ORIGINAL popin, because `popinClose` early-bails on a redirecting popin — that close was a silent no-op, leaving the original open behind the new one (browser-verified: pre-fix stays open, post-fix closes). MEASURED CORRECTION: the timer was NOT load-bearing for the different-popin branch as long assumed — that branch threw first, because the published `gina.popin` WAS re-assembled with a target-wins merge on every `new Popin()`, so its `getPopinByName`/`getPopinById` stayed bound to the FIRST instance's registry and resolved ONLY the boot popin, while `$popins` + `getActivePopin` saw every popin and `gina.popin.activePopinId` never updated (so `getActivePopin()` returned null once nothing was open) — and popins registered AFTER an instance's publish (click-time in-page dialog registrations) never reached the published registry at all, with destroyed popins lingering in it. FIXED as #B90 (2026-07-10): ONE module-scoped registry (`_sharedPopins`) aliased by every instance's `$popins`; a publish-ONCE `gina.popin = instance` (the published object IS the first instance, LIVE — re-publishing was both the freeze defect and, with a shared registry, a self-merge recursion hazard: gina's merge deep-recurses plain objects and `$popin.eventData` can hold the $popin itself); and a `setActivePopinId` write-through helper at all 7 write sites keeping the published `activePopinId` truthful no matter which instance opens or closes a popin. Browser-verified both ways on the built bundle (pre-fix: accessors blind + the validator cross-popin redirect 422-throws `not found`; fixed: accessors resolve every instance's popins and a form submit redirecting into a DIFFERENT popin works end-to-end — the original closes, the target loads content-first, `activePopinId` follows). Generalises: an accessor published through a target-wins `merge()` silently keeps the first instance's closure — publish a live instance once and share module-scoped state instead of re-merging per construction; and never infer a code path is reachable from the fact that it exists. (5) EAGER content warm — `data-gina-dialog-preload="eager"` (case-insensitive) opts a trigger into a one-shot idle warm-all pass: after `window` load, on requestIdleCallback (setTimeout fallback), serialized one GET at a time, routed through the SAME shared per-trigger gate as the hover warm (`warmTrigger`: disabled skip → the `"false"` opt-out → URL-cache dedup → in-flight reserve + fetch) so the two warm paths cannot drift and whichever fires second is a no-op; skipped entirely under Save-Data; triggers injected after the pass keep the delegated hover warm; staleness matches the shipped no-TTL hover semantics — the opt-in accepts the wider warm→open window. (6) #B139 (2026-07-20, rides the next cut): the content cache no longer outlives the open it warmed — measured pre-fix it was a ONE-GENERATION-LAGGING store (every open after the first paid ~1 GET yet rendered the PREVIOUS open's fetch: the leftover entry — the #B54 in-flight adoption's repopulated body, or the around-open re-warm — blocked the fresh warm's dedup; NO invalidation path existed, close/unbind/destroy never touched the cache; reproduced back to 0.5.4, so it predates #B54 and the eager pass). Fix: close clears the popin's content-URL slot (stamped at consumePreload both branches + popinLoad) AND re-sweeps the same slot ~120ms later, because the close-time a11y focus-return and pointer re-hover fire TRUSTED synthetic intents that re-warm the slot with close-era content within 1ms of the close (measured; a raced sweep is benign by construction — at most one extra fetch, never staleness, since an adopted in-flight body still reaches its open through the waiter chain). AND `preload="false"` triggers now skip the cache READ at open on both open paths — `false` is the hard always-refetch spelling for volatile popins (its GET always happens at open, never from a same-URL sibling's warm); no new annotation vocabulary. Warmed-never-opened entries keep the page-lifetime eager semantics above; default triggers pay at most one extra idempotent GET per close (the swept synthetic warm). Content preload deliberately does NOT ride 103 Early Hints: popin responses vary on `X-Requested-With` (a non-XHR GET of a fragment layout gets the iframe-wrap variant and misses the scripts append), and a browser `Link rel=preload` fetch never carries that header — wrong-variant bytes or a double GET, with no success branch; `self.setEarlyHints()` remains the right tool for a popin's STATIC subassets (the CSS/JS it injects via getScript/getStyle). (7) #A11Y8 (2026-08-05, rides the next cut): the background-`inert` loop in `applyNonModalShims` skipped `instance.target` — the shared `.gina-popins` container — and since every popin lives INSIDE that container, and `popinOpen` never closes the popin it supersedes (it only overwrites `activePopinId`), opening a second NON-MODAL dialog left the first one fully keyboard-reachable behind it: a user could tab out of the dialog on screen and into a stale one. Non-modal is the FRAMEWORK DEFAULT for `data-gina-dialog` (`resolveModal` falls through to `return false` at step 5; only a legacy `data-gina-popin-name` trigger is unconditionally modal), so this was the ordinary path, not an exotic one — note item (2)'s "native modals in EVERY env" predates the non-modal branch and now describes LEGACY triggers only. Fix: descend into the container instead of skipping it, and inert sibling `dialog[open]` ONLY — a `<dialog>` without `open` is already `display:none` per the UA stylesheet, so it is unreachable without help (measured in Chrome with controls firing both directions; the modal path was never affected, native `showModal()` handles the top layer itself). Teardown needed NO change: `removeNonModalShims` sweeps `[data-gina-popin-inert]` document-wide, so dialogs nested in the container restore without it knowing they exist. The `getAttribute('inert') == null` guard means gina only marks what it set, so a consumer-set `inert` is never stamped and never stripped. All seven are browser-bundled — prod dist rebuild required; verify (2), (3), (4) and (5) with a REAL preemptive-open / preload / redirect-tunnel consumer — a minimal smoke page is falsely green; (7) needs TWO popins open at once, the only scene in which it differs at all.
904
904
 
905
905
  214. **Per-request routing-clone correctness — narrow the defensive clone, and propagate mutations onto returned clones (consolidates former #194/#195; #B52-residual, 2026-06-18).** The router's per-request `options.conf = JSON.clone(conf)` — a multi-MB deep clone of the ENTIRE bundle config per matched request, held for the request's whole query+render window — was narrowed to: shallow-copy the top level + `conf.content`, deep-clone ONLY `conf.content.routing`, the one subtree actually MUTATED per request (audited: every other per-request write is a whole-subtree REASSIGNMENT a shallow copy isolates, and plugins/connectors/models read config via their own clones, never through the request's) — removing a dev/high-load heap high-water-mark proportional to config size (measured 163→62 MB peak under 150 concurrent slow requests, drains on idle). Rule: clone only the subtrees mutated per request and share the immutable remainder by reference — but audit EVERY per-request writer first, or a missed mutate-into-a-shared-subtree reintroduces cross-request contamination. Sibling: `getRouteByUrl` (server-side redirect/relative-route resolution AND browser-bundled) aliased the shared route config's `param` by REFERENCE while the matcher rewrites `param.{path,namespace,file,title}` in place for `:placeholder` routes — request A's substitution contaminated request B (server) and repeat navigations (client). The fix is TWO coordinated edits, not just "clone the alias": clone `param` per request AND propagate the substituted param onto the returned route object — the function returns a fresh clone of the SINGLETON, so cloning alone returns the un-substituted `:placeholder` and regresses even the first request. Rule: when a matcher both aliases a shared config object and returns a re-clone of that object, isolating the alias is insufficient — propagate the per-request mutations onto the returned clone. The sibling is browser-bundled → prod dist rebuild.
906
906
 
@@ -924,7 +924,7 @@ Dev-mode query instrumentation captures every database query tied to the current
924
924
 
925
925
  239. **Native schema/DTO builder `lib/dto` (`require('gina').dto`) — JSON-Schema-canonical, three projections (#DTO1 primitive, 0.5.18-alpha.2).** Author a data shape ONCE: `dto.object({ email: dto.string().email().required(), age: dto.integer().min(0).max(120), role: dto.enum(['admin','user']).required() }).as('CreateUser')`. Projects to (a) dialect-aware JSON Schema — `.toJsonSchema('draft-07'|'2020-12'[,{standalone}])`, the IDENTITY/canonical form for OpenAPI 3.1 requestBody/responses + MCP inputSchema; (b) `.toRules()` — the LIVE form-validator rules-object (the SAME engine routing `validator::` + `forms/rules/*.json` use, so a DTO unifies validation rather than adding a parallel validator; measured returning `{isValid():fn, error:{field:{rule:msg}}, data:coerced}` with coercion + `exclude` stripping); (c) `.name` — stable id for the type generator + a route's `param.dto`. Curated vocabulary EXCLUDES `toFloat` (server crash, `form-validator.js:1514`) + `query` (spins a Controller). Zero runtime deps, CJS/`var`; named registry on `process.gina._dtos` (survives dev hot-reload — plain-`require`d in `lib/index.js` for stable `instanceof`). **⚠️ VALUE-RANGE (`.min()/.max()`) is SCHEMA-ONLY in this cut** — carried as `minimum`/`maximum` for OpenAPI/MCP fidelity but NOT runtime-enforced: the `is` evaluator (`form-validator.js:1115`) accepts only a single binary comparison (no `&&`, no `this.value`) and a `$field` self-ref CRASHES the server form-body path (`main.js:7166-7172` reads `$fields.count()` on the null server `$fields` — a #B85-sibling), so `toRules()` compiles NO `$` of its own; runtime value-bound = a follow-on (a first-class engine rule, browser-bundled → dist rebuild). **⚠️ But an AUTHORED `$` can still reach the rules** — via an enum VALUE (`dto.enum(['$100'])`), a field NAME, or a date mask — and ANY `$` in the stringified rules sends the engine down that same null-`$fields` crash path (measured), so `toRules()` now THROWS on one (`toJsonSchema()` is deliberately NOT guarded — a `$` is valid in a JSON Schema enum, so a currency-style DTO still documents and still serves as a `param.responseDto`). **⚠️ A required field the client OMITS entirely PASSES** (isRequired fires only on present-but-empty, not a missing key) — a server-side validation driver MUST inject `''` for rule-declared fields absent from the payload before validating. Also un-collapses inline `validator::{...}` routing requirements into real JSON Schema via `lib.routingIntrospect.requirementToSchema()` (pure; the OpenAPI `.*` fix). Consumed by `bundle:openapi`/`bundle:mcp` (#DTO1c) + the default-on #DTO2 422 validation pipe (next). Server-side — NO dist rebuild. Tests: `test/lib/dto.test.js` (37, behavioural-through-the-real-engine + subtract) + `routing-introspect.test.js`.
926
926
 
927
- 240. **DTOs drive `bundle:openapi` + `bundle:mcp` schemas via a `dtos/<Name>.js` bundle convention (#DTO1c, 0.5.18-alpha.2).** A route declares `param.dto` / `param.responseDto` (names) in `routing.json`; the generators resolve each to `<bundle>/dtos/<name>.js` via `dto.load(bundleSrcPath, name)` — a new `lib.dto` method: require the file → `mod instanceof DtoObject ? mod : mod(dto)` (factory) → stamp+register → registry-fallback → null; fail-soft (a broken factory THROWS, the caller warns+skips), no `require.cache` eviction. **⚠️ A bundle DTO file MUST use the FACTORY shape `module.exports = function (dto) { return dto.object({...}, 'Name'); }`, NOT `require('gina').dto`** — because the OFFLINE `bundle:openapi`/`bundle:mcp` bootstrap via the lib registry (`bin/cli`), NOT `core/gna.js`, so a `require('gina')` inside a DTO file cold-loads gna.js with no bundle context and THROWS at `core/gna.js:174` (`ctxObj.paths` null). `require('gina').dto` works only at request-time in a booted bundle; the factory works in both (measured — the naive shape's offline throw was reproduced, the factory proven end-to-end). openapi (3.1 → JSON Schema 2020-12): `param.dto` on post/put/patch → `requestBody` + a `422`; `param.responseDto` → the 200 `content`. mcp (draft-07): `param.dto` → `inputSchema.body` on non-GET (else the lenient placeholder); `param.responseDto` → `outputSchema` (the runtime MCP server already passes it through). Both also un-collapse an inline `validator::{...}` URL-param requirement into a real schema fragment (the `.*`-collapse fix). **#B201 (0.6.3) closed three forms that un-collapse silently DROPPED:** the scalar `"isString": N` now emits `minLength` (it was array-only, so even the docs' own scalar example produced nothing), and `isInteger`/`isNumber` digit bounds now emit a `description` + a namespaced `x-gina-digitBounds` `{min?,max?}` extension. Those bounds constrain the value's STRING-FORM length (a negative sign counts), not its range, so they are deliberately NOT mapped to `minimum`/`maximum` — wrong for every negative, since digits `[2,4]` admit `[-999,-1] ∪ [10,9999]`, which no single range expresses — nor to `minLength`/`maxLength`, which are string-only keywords a validator ignores on a numeric type, i.e. the schema-level twin of #B198's "a documented bound nothing enforces". Both generators copy fragment keys verbatim, so annotations flow with zero consumer change and bare-`true` rules stay byte-identical. Server-side/CLI-only — NO dist rebuild. Verified end-to-end against the real offline CLI (a scaffold bundle emitted a real requestBody / inputSchema.body / outputSchema / un-collapsed param). Tests: `test/lib/{bundle-openapi,bundle-mcp}.test.js` (source-pins + a source-locked replica through the real dto + introspect) + `dto.test.js §08` (dto.load). **Response-side emission drops `.exclude()`d fields (#B110, 0.5.18-alpha.2):** both response sites emit `toJsonSchema(dialect, { dropExcluded: true })` — an excluded field can never reach the wire (the render transform deletes it before the single stringify), so the 200 `content` / `outputSchema` must not advertise it in `properties` NOR `required[]` (an emptied `required` is deleted); request-side `requestBody`/`inputSchema.body` keep the declared shape (the client DOES send it). The drop reads the SHAPE (`_excluded`), never `toRules()` (stays total for an authored-`$` DTO), and the render-json dev missing-required warn checks the same response projection — a `.required().exclude()` field an action omits no longer warns. Tests: `dto.test.js §11` + `bundle-openapi.test.js 01.7/02.9` + `bundle-mcp.test.js 02.9` + `render-json.test.js §06`. **#MS4 (2026-07-24, unreleased) adds the authorization contract to `bundle:openapi`:** gated routes (`param.requireAuth === true`, a non-empty `param.roles`, or a non-empty `param.policy` — the exact runtime authz-gate predicate; a truthy-STRING `requireAuth` does not gate, matching the runtime) emit a `401` response entry (+ a `403` only when roles/policy add authorization beyond authentication — role/policy NAMES never reach the spec, mirroring the runtime's generic 403 bodies and client-map stripping), and when machine-caller auth is EFFECTIVELY configured (`auth.machine.enabled === true` strictly AND callers non-empty OR authenticator set — fail-closed, a `"true"` string emits nothing) the spec gains `components.securitySchemes.bearerAuth` (`http`/`bearer` — matching the machine 401's own `WWW-Authenticate: Bearer` challenge; its description notes a custom authenticator may accept other credential shapes and that a signed-in session also satisfies gated routes) + per-operation `security: [{bearerAuth: []}]` on gated routes only, never a top-level `security`. NO session-cookie scheme: the cookie name is app-owned (set in app code, unreadable offline) — session-only gated routes carry the credential story in the 401 description instead. Enabler: the previously-dormant settings.json read switched from a plain `require` (throws on the comment lines real settings.json files carry, silently leaving `settings` null) to `requireJSON` + a parse-failure warn. An un-gated, un-configured bundle's spec is byte-unchanged. Tests: `bundle-openapi.test.js` §03-§04 (source pins + executing the shipped helper bytes via control-gated extraction, incl. the role-name-absence and truthy-string-fail-closed cases).
927
+ 240. **DTOs drive `bundle:openapi` + `bundle:mcp` schemas via a `dtos/<Name>.js` bundle convention (#DTO1c, 0.5.18-alpha.2).** A route declares `param.dto` / `param.responseDto` (names) in `routing.json`; the generators resolve each to `<bundle>/dtos/<name>.js` via `dto.load(bundleSrcPath, name)` — a new `lib.dto` method: require the file → `mod instanceof DtoObject ? mod : mod(dto)` (factory) → stamp+register → registry-fallback → null; fail-soft (a broken factory THROWS, the caller warns+skips), no `require.cache` eviction. **⚠️ A bundle DTO file MUST use the FACTORY shape `module.exports = function (dto) { return dto.object({...}, 'Name'); }`, NOT `require('gina').dto`** — because the OFFLINE `bundle:openapi`/`bundle:mcp` bootstrap via the lib registry (`bin/cli`), NOT `core/gna.js`, so a `require('gina')` inside a DTO file cold-loads gna.js with no bundle context and THROWS at `core/gna.js:174` (`ctxObj.paths` null). `require('gina').dto` works only at request-time in a booted bundle; the factory works in both (measured — the naive shape's offline throw was reproduced, the factory proven end-to-end). openapi (3.1 → JSON Schema 2020-12): `param.dto` on post/put/patch → `requestBody` + a `422`; `param.responseDto` → the 200 `content`. mcp (draft-07): `param.dto` → `inputSchema.body` on non-GET (else the lenient placeholder); `param.responseDto` → `outputSchema` (the runtime MCP server already passes it through). Both also un-collapse an inline `validator::{...}` URL-param requirement into a real schema fragment (the `.*`-collapse fix). **#B201 (0.6.3) closed three forms that un-collapse silently DROPPED:** the scalar `"isString": N` now emits `minLength` (it was array-only, so even the docs' own scalar example produced nothing), and `isInteger`/`isNumber` digit bounds now emit a `description` + a namespaced `x-gina-digitBounds` `{min?,max?}` extension. Those bounds constrain the value's STRING-FORM length (a negative sign counts), not its range, so they are deliberately NOT mapped to `minimum`/`maximum` — wrong for every negative, since digits `[2,4]` admit `[-999,-1] ∪ [10,9999]`, which no single range expresses — nor to `minLength`/`maxLength`, which are string-only keywords a validator ignores on a numeric type, i.e. the schema-level twin of #B198's "a documented bound nothing enforces". Both generators copy fragment keys verbatim, so annotations flow with zero consumer change and bare-`true` rules stay byte-identical. Server-side/CLI-only — NO dist rebuild. Verified end-to-end against the real offline CLI (a scaffold bundle emitted a real requestBody / inputSchema.body / outputSchema / un-collapsed param). Tests: `test/lib/{bundle-openapi,bundle-mcp}.test.js` (source-pins + a source-locked replica through the real dto + introspect) + `dto.test.js §08` (dto.load). **Response-side emission drops `.exclude()`d fields (#B110, 0.5.18-alpha.2):** both response sites emit `toJsonSchema(dialect, { dropExcluded: true })` — an excluded field can never reach the wire (the render transform deletes it before the single stringify), so the 200 `content` / `outputSchema` must not advertise it in `properties` NOR `required[]` (an emptied `required` is deleted); request-side `requestBody`/`inputSchema.body` keep the declared shape (the client DOES send it). The drop reads the SHAPE (`_excluded`), never `toRules()` (stays total for an authored-`$` DTO), and the render-json dev missing-required warn checks the same response projection — a `.required().exclude()` field an action omits no longer warns. Tests: `dto.test.js §11` + `bundle-openapi.test.js 01.7/02.9` + `bundle-mcp.test.js 02.9` + `render-json.test.js §06`. **#MS4 (2026-07-24, unreleased) adds the authorization contract to `bundle:openapi`:** gated routes (`param.requireAuth === true`, a non-empty `param.roles`, or a non-empty `param.policy` — the exact runtime authz-gate predicate; a truthy-STRING `requireAuth` does not gate, matching the runtime) emit a `401` response entry (+ a `403` only when roles/policy add authorization beyond authentication — role/policy NAMES never reach the spec, mirroring the runtime's generic 403 bodies and client-map stripping), and when machine-caller auth is EFFECTIVELY configured (`auth.machine.enabled === true` strictly AND callers non-empty OR authenticator set — fail-closed, a `"true"` string emits nothing) the spec gains `components.securitySchemes.bearerAuth` (`http`/`bearer` — matching the machine 401's own `WWW-Authenticate: Bearer` challenge; its description notes a custom authenticator may accept other credential shapes and that a signed-in session also satisfies gated routes) + per-operation `security: [{bearerAuth: []}]` on gated routes only, never a top-level `security`. NO session-cookie scheme: the cookie name is app-owned (set in app code, unreadable offline) — session-only gated routes carry the credential story in the 401 description instead. Enabler: the previously-dormant settings.json read switched from a plain `require` (throws on the comment lines real settings.json files carry, silently leaving `settings` null) to `requireJSON` + a parse-failure warn. An un-gated, un-configured bundle's spec is byte-unchanged. Tests: `bundle-openapi.test.js` §03-§04 (source pins + executing the shipped helper bytes via control-gated extraction, incl. the role-name-absence and truthy-string-fail-closed cases). **The contextless-`require('gina')` boundary is INTENDED but was previously unrecoverable and undiagnosable (#B276+#B277, 0.6.4-alpha.2).** `index.mjs`'s own docblock has always stated it — *"Importing outside a spawned bundle child … the framework bootstrap expects the bundle context and throws without it"* — and both published entries (`.` → `core/gna.js`, `./gna` → the root barrel, each with shipped `.d.ts` + ESM twins) throw alike. Two defects sat on top of that correct boundary. **(1) The throw was uncatchable.** The logger is a load-time singleton (`lib/index.js` requires it eagerly, `main.js` calls `Logger()` at module scope) and its default flows are `['default','mq']`, so `speaker.js` opens a TCP socket BEFORE the boot reaches its throw — and that socket kept the event loop alive, so try/catch caught the error and the process still could not exit. **The hang is LISTENER-CONTINGENT** (measured both ways with two-sided controls): nothing bound to the MQ port ⇒ ECONNREFUSED, handle closes, exit 0 — so the bug is invisible on a quiet machine and hangs with no output on one running bundles or in CI. Fixed by `client.unref()`: a logging transport must never be why a process stays alive. ⚠️ **The socket still CONNECTS — `unref` changes only whether it votes on process lifetime** (consumer-verified 2026-08-05 against a live listener: the `[MQSpeaker] connected to server` line still logs, and `getActiveResourcesInfo()` shows NO `TCPSocketWrap` at 600ms after the connect demonstrably completed). So anyone counting inbound connections on the MQ side still sees them; "unref'd" is not "suppressed". Blast radius measured near-zero — 113/137 CLI handler files already `process.exit()` (so they truncate flushes today regardless), a booted bundle holds the loop via its own listener (`core/server.js` owns the single `listen()` for both engines), and the file container's delayed-output path is opt-in with its own client. ⚠️ The consumer-visible workaround `GINA_LOG_STDOUT=true` does splice `mq` out of the flows, but it ALSO flips log format to JSON (measured) — a container logging-mode flag pressed into service as a "don't open a socket" switch. **(2) The message named an internal call, not the boundary** — `setPath("gina.home", path): path cannot be empty or undefined`, because `getEnvVar` reads `process.gina` ONLY (never `process.env`, by design) and is empty outside the CLI. It now names the boundary and routes the reader to `SuperController.createTestInstance()`, which IS reachable outside a bundle (`core/controller` loads standalone) and is the supported way to exercise controller code without booting. ⛔ **Do NOT "fix" this by extending the `getEnvVar → process.env` ladder to `gna.js`, nor by skipping the `setPath` when empty** — REFUTED BY MEASUREMENT: satisfying that call merely defers the failure 8 lines to `ReferenceError: GINA_HOMEDIR is not defined` (a bare global), strictly less legible, with `projects[projectName].path` failing next; and that `setPath` is the SOLE registration site for `gina.home` repo-wide (the SQLite connector and session store read it), so skipping it leaves it unregistered. Failing fast at the first point contextless boot is detectable is correct. Tests: `test/lib/logger-mq-speaker-unref.test.js` (comment-stripped source pin + a behavioural arm driven against an isolated `GINA_HOMEDIR` and an ephemeral listener, carrying its own must-be-killed control; the pin is comment-stripped because subtracting the fix left a bare existence pin GREEN against `// client.unref();`).
928
928
 
929
929
  241. **`helpers/context.js` `getLib`/`getConfig` no longer crash with `Cannot read properties of undefined (reading 'conf')` on a PARTIAL Config (module-load / daemon-spawned bootstrap) — the resolver-layer sibling of the #236 detached-`throwError` fix (0.5.18-alpha.2).** `core/config.js` populates `envConf` (`Env.load`, `:391`) BEFORE `bundlesConfiguration` (assigned at `:243-250` only after `loadBundlesConfiguration` succeeds), and `bundlesConfiguration.conf` is an ALIAS of `envConf` (`:247` `conf: self.getInstance()`). When the async Config build ABORTS before completing — `config.js:223 if (err) { …; setTimeout(() => process.exit(1), 0); return; }` returns BEFORE the `:243` assignment (canonically a fail-closed `secrets.resolve` throws on an unset `${secret:KEY}` at `:2811`) — a detached caller (a bundle `onInitialize`, a cron, a bootstrap `getLib()`) observes a PARTIAL Config: `envConf` valid, `bundlesConfiguration` + `Config.initialized` undefined. `getLib` (`:600`) and both `getConfig` branches (`:465` with-confName, `:478` without) dereferenced `conf.bundlesConfiguration.conf[bundle][env]…` UNCONDITIONALLY there → the `reading 'conf'` TypeError → the #236 detached-path `throwError` re-threw → uncaughtException → SIGKILL, MASKING the real boot error. **Grep-PROVEN surface (not whack-a-mole):** a framework-wide grep of `bundlesConfiguration` reads (`core/ helpers/ lib/`, excl. dist) — the ONLY unguarded live consumers are `getLib` + `getConfig`; `config.js`'s own reads are guarded (`:3140 if (Config.initialized && self.bundlesConfiguration)`) or during-build; `lib/routing/src/main.js:1075` is commented-out (dead); 14/16 derefs are `.conf` (=== envConf). **Fix:** ONE shared `resolveBundlesConf(conf)` (`bundlesConfiguration.conf` when built, else the equivalent `envConf`, else null) consumed by `getLib` + BOTH `getConfig` branches so the two siblings cannot drift; PLUS fail-clean — `getConfig`'s two catches, `getLib`'s outer catch and its unresolvable-else all pass `throwError(…, isFatal=true)` (emerg-log + return, never a bare throw that escalates to SIGKILL). Byte-identical on the healthy path (bundlesConfiguration present); only the previously-crashing partial-Config path changes. **The partial Config is usually a SYMPTOM** — something upstream aborted the build (canonically a fail-closed `${secret:KEY}` resolution) — so hardening the resolvers UN-MASKS that real, actionable error (e.g. `Secret resolution failed for <KEY>`) instead of hiding it behind a `reading 'conf'` crash; the abort itself (e.g. secrets not wired for a deploy) is the caller's config to fix. Rule: a global config resolver reachable from a detached / module-load context must accept a partial Config (bundlesConfiguration may not be built yet; fall back to the aliasing envConf) and fail clean when neither source is usable. Server-side only — NO dist rebuild. Tests: `test/core/context-config-resolution.test.js` (source pins on the shared resolver + both resolvers routed through it + isFatal catches; full/partial/broken replicas; a subtract reproducing the exact crash pre-fix). Commit `b2dc67da`, established 2026-07-14.
930
930
 
@@ -986,7 +986,7 @@ Dev-mode query instrumentation captures every database query tied to the current
986
986
 
987
987
  272. **Staged-upload client layer stores binary files byte-identical — the hand-assembled DOMString multipart body UTF-8-inflated every file byte ≥ 0x80 (#B148, fixed 2026-07-22, unreleased — rides the next cut).** `processFiles()` historically read each staged File (`FileReader.readAsArrayBuffer`), converted the buffer to a per-byte JS string (`ab2str`) and string-concatenated the whole multipart body, which `send()` transmitted as `xhr.send(<DOMString>)` — a DOMString is UTF-8-encoded on the wire, so every byte ≥ 0x80 became a 2-byte sequence: any real binary upload (image/PDF/archive) was stored inflated + corrupted server-side (measured ×1.49 on a cycling-byte fixture; a real PNG's 0x89 signature byte arrived as 0xC2 0x89 and no longer parsed; drop and native-picker paths identically). CRITICALLY, the corruption only STARTED at 0.5.22: the pre-#B103 server's two since-removed string-decode layers (`setEncoding('utf8')` + the liner's `toString()`/charCode-mod-256 re-encode) EXACTLY reversed this client inflation at every chunking — two wrongs cancelling — so ≤ 0.5.21 round-trips were byte-clean and the #B103 server fix exposed the client defect. Fix: the body is assembled as a `Blob` — [field parts (string), per-file header (string), the File object itself (raw bytes), CRLF, …, closer] — with the multipart FRAMING byte-identical (same boundary delimiters, same `name=`/`group=`/`filename=` disposition-parameter set = the documented upload-group wire vehicle, same per-part headers; `Content-Length` now carries `File.size`, numerically identical to the historical char count): zero server change, zero wire-contract change; `xhr.send(Blob)` transmits verbatim and the explicitly-set multipart `Content-Type` header survives; the `FileReader`/`ab2str` stage is retired (its only consumer was the wire — previews come from the server response, not a client read). Disposition-param values (`name`/`group`/`filename`) are now percent-escaped for CR/LF/double-quote per RFC 7578 §5.1.1 (an unescaped quote produced a malformed part). Files corrupted by this defect are LOSSLESSLY recoverable — the stored bytes are exactly the UTF-8 encoding of the original byte sequence: decode utf8 → re-encode latin1 restores the exact original (all 256 byte values round-trip). Browser-bundled (validator plugin) ⇒ consumers pick the fix up with a bundle RE-BAKE, and the fixed client must pair with a server ≥ 0.5.22 (an older server's decode layers would corrupt the now-raw bytes — the #B103 corruption in reverse). Tests: validator-upload-binary-wire.test.js (source pins + a control-gated extract+eval of the shipped assembly asserting exact whole-body bytes + a round-trip through the real busboy fork incl. the `group=` param and a UTF-8 filename + a frozen pre-fix subtract reproducing the inflation + red-first dist pins).
988
988
 
989
- 273. **Error responses carry a first-class incident ref (#ERRREF).** Both `throwError` builders (the engine-attached one in core/server.js + the controller-side `this.throwError`) mint a short incident ref per error — 6 uppercase hex from `crypto.randomBytes(3)`, or a relay-safe caller-supplied `ref` (charset `[\w.-]`, max 32, the X-Request-Id sanitize discipline; anything else re-mints, so log forging is neutralized) — returned as a top-level `ref` field on every JSON error body in ALL scopes (a random correlation key, no server detail; the stack-egress gate still strips `stack` outside local scope), stamped on the custom-error-page data (`eData.ref`) and the inline HTML fallback pages, and paired server-side in ONE error-level log line per thrown error (`[ ref XXXXXX ][ req <requestId> ] METHOD [ code ] url` + full message/stack/cause) emitted BEFORE the wire sanitize — every scope, production included, composing with the always-on request correlation id instead of duplicating it. Consolidates the former in-gate stack-egress log emits + per-branch summary lines into one line per error, and closes two gaps: the controller JSON/XHR branch stripped the stack with NO pre-strip log (a production API error's stack was lost server-side entirely), and the server XHR summary line logged no message. `Error.cause` reaches the log, never the wire. Always-on, no config; additive/back-compat on the wire. The two mint copies are kept byte-identical (test-pinned). Server-side only — no dist rebuild; running bundles pick it up at restart. Tests: test/core/error-ref.test.js (36 — control-gated extraction-executed compositions of the real bytes + a frozen pre-fix subtract) with server-throwerror-stack-gate.test.js §02 realigned to the log-free helper.
989
+ 273. **Error responses carry a first-class incident ref (#ERRREF).** Both `throwError` builders (the engine-attached one in core/server.js + the controller-side `this.throwError`) mint a short incident ref per error — 6 uppercase hex from `crypto.randomBytes(3)`, or a relay-safe caller-supplied `ref` (charset `[\w.-]`, max 32, the X-Request-Id sanitize discipline; anything else re-mints, so log forging is neutralized) — returned as a top-level `ref` field on every JSON error body in ALL scopes (a random correlation key, no server detail; the stack-egress gate still strips `stack` outside local scope), stamped on the custom-error-page data (`eData.ref`) and the inline HTML fallback pages, and paired server-side in ONE error-level log line per thrown error (`[ ref XXXXXX ][ req <requestId> ] METHOD [ code ] url` + full message/stack/cause) emitted BEFORE the wire sanitize — every scope, production included, composing with the always-on request correlation id instead of duplicating it. Consolidates the former in-gate stack-egress log emits + per-branch summary lines into one line per error, and closes two gaps: the controller JSON/XHR branch stripped the stack with NO pre-strip log (a production API error's stack was lost server-side entirely), and the server XHR summary line logged no message. `Error.cause` reaches the log, never the wire. Always-on, no config; additive/back-compat on the wire. The two mint copies are kept byte-identical (test-pinned). Server-side only — no dist rebuild; running bundles pick it up at restart. Tests: test/core/error-ref.test.js (36 — control-gated extraction-executed compositions of the real bytes + a frozen pre-fix subtract) with server-throwerror-stack-gate.test.js §02 realigned to the log-free helper. **#A11Y3 (2026-08-04, unreleased) — those inline HTML fallback pages are now CONFORMING DOCUMENTS.** A per-file `a11yErrorDocument(code, bodyHtml, req)` helper (duplicated in core/server.js + controller.js so each error path stays self-contained) wraps the existing body in `<!doctype html><html lang="…"><head><title>Error N</title></head><body><main>…</main></body></html>`; the body markup — and therefore the `ref` above — is embedded VERBATIM, so nothing in this entry's contract changes. It fixes a real DEFAULT-PATH defect, not an edge case: the HTTP/1.1 branch closed `<body>` with an *opening* `<html>` tag (so the document never terminated) and both branches opened two `<pre>` while closing one, and a default scaffolded project ships no `templates/html/errors/` directory — `errorFiles` is populated only by config.js's readdir of that path — so `hasCustomErrorFile` is false and this markup IS what an unhandled 500 serves. (`core/template/error/` exists but has ZERO readers; it is not scaffolding.) A second trigger: even a project WITH error pages falls back here when the error came from `processMiddlewares`. `lang` comes from `a11yLangTag(req)` — negotiated culture normalized to BCP-47 (`en_CM`→`en-CM`, q-value stripped, `_`→`-`), falling back to `en` rather than emitting a tag AT cannot parse; the same normalization now guards `page.view.lang` and the three render-nunjucks error fallbacks, and adds doctype+`lang` to render-swig's layoutless/iframe wrapper. The locale LOOKUP deliberately keeps the raw underscore form (its resolution behaviour is a separate concern, and `getEnvVar('GINA_CULTURE') || 'en_CM'` is source-pinned) — only what reaches the document is normalized. ⚠️ Two hazards for anyone editing these sites: the layoutless wrapper's `</body>` is load-bearing beyond markup validity (release-banner's `maybeInject()` splices the dev banner on `/<\/body>/i`, and no test would catch its loss), and `controller.test.js` source-pins the literal `var msgString = '<h1 class="status">Error ` — so the wrap must stay a SEPARATE statement at the `res.end()` site, never prepended to that assignment. `helpers/context.js`'s HTML error branch was deliberately NOT touched: `var isUsingTemplate = isUsingTemplate` makes it permanently `undefined`, so that branch is unreachable dead code and the file always answers `application/json`. Server-side only — no dist rebuild; running bundles pick it up at restart. Full suite 14546/14546 green.
990
990
 
991
991
  274. **Staged-upload client layer: the action-attribute fallback no longer clobbers the staging action, a missing preview element no longer crashes the success handler, and an upload-only input no longer logs a spurious warn+error pair on every bind (#B146/#B147 2026-07-22 + #B149 2026-07-23, unreleased — ride the next cut).** Two LOW `data-gina-form-upload-*` defects surfaced by the #R8 smoke, both in `core/plugins/lib/validator/src/main.js`. **#B146:** `checkUploadUrlActions`'s `checkAction` default-route fallback hardcoded `setAttribute('data-gina-form-upload-action', route.toUrl())` regardless of which attribute it was checking — so a file input declaring only its staging action had it silently repointed at the resolved reset/delete default route (`upload-delete-from-tmp-xml`), sending the staging POST to the delete route; it compounded to a SILENT failure when `toUrl()`'s absolute origin (e.g. 127.0.0.1 vs localhost) tripped a CORS preflight → XHR status-0 → the commented-out status-0 branch in `send()`. Fixed by writing the attribute actually being checked: `setAttribute(action, …)`. The origin/CORS half is deliberately DEFERRED — `toUrl()` always prepends the hostname (no clean origin-less form: `.url` diverges from the declared-attribute path and skips webroot normalization), and the real silent-death is the disabled status-0 branch in `send()`, a separate item. **#B147:** `previewContainer` is a `getElementById` RESULT (element|null), but the guard read `typeof(previewContainer) != 'undefined'` — and `typeof null === 'object'`, so a preview-element MISS set `hasPreviewContainer` true, stored a null container, and the success handler later dereferenced `.id` (TypeError on any upload configured without a preview target). Fixed with the typeof-null guard pattern: `&& previewContainer`. **#B149** (2026-07-23, `986c5b9d`): the SAME `checkAction` checks all three action attributes, but the switch defaults only `-action`/`-reset-action` — `-delete-action` (removes an ALREADY-SAVED file, an app-specific endpoint, so deliberately no framework default) fell through to the error branch on every bind of a working upload-only input, emitting a `console.warn` ("Trying to get default route" — when there is none) + `console.error` + a visible `$errorContainer.innerHTML` write. Fixed quiet-at-bind: a no-default + absent action is a single `console.debug` + early `return`; a WITH-default action that genuinely fails to resolve STILL errors (surgical); the delete requirement stays enforced lazily at the use site (`onUploadResetOrDelete` throws when a delete fires with no URL). Browser-bundled (validator plugin) ⇒ consumers pick all three up with a bundle RE-BAKE; the server half is untouched. Tests: `test/core/validator-upload-action-guard.test.js` (source pins + an extract+eval of the shipped `checkAction` proving the reset-fallback no longer clobbers the staging action + a frozen #B146 subtract + a #B147 guard replica/subtract + the #B149 quiet-at-bind suites 06-08 (debug+return pins, recording-console behaviour, dist fidelity) + red-first dist pins).
992
992
 
@@ -998,7 +998,7 @@ Dev-mode query instrumentation captures every database query tied to the current
998
998
  278. **A cache key is opaque DATA — never route it through an expression/condition evaluator.** An output-cache key embeds the request URL, so a querystring's `?`/`=` parse as OPERATOR tokens in any condition-DSL lookup — the render cache's event registry originally lived in a collection whose dedup ran keys through the condition evaluator, and the SECOND cache-miss render of any cached GET route with `invalidateOnEvents` + a querystring THREW (in the swig path that rejection unwound to the function-level try, whose catch answered 500 — discarding an already-rendered page). The registry became a plain array matched with `===`. Generalizes beyond caching: never let a value carrying user-controlled characters reach a query/condition DSL as an expression — match registry rows with `===` on plain structures, and treat every composite key as an opaque string.
999
999
 
1000
1000
  279. **Transient-vs-permanent connector-error classification (#CE1, `26d803bd`+`d9231eff`, 0.5.25).** `lib/connector-error` (`classify(err) → {isTransient, reason}` + in-place `stamp(err)`) stamps `err.isTransient` (boolean; explicit `false` = classified-permanent, not merely absent) + `err.transientReason` (normalized token, `null` when permanent) on every datastore query error reaching the controller, so an app renders an honest retryable response by branching on the CONDITION instead of string-matching vendor text. Wired at 2 error sites per connector (Promise reject + direct-callback) across couchbase/mongodb/mysql/postgresql/scylladb/sqlite; `stamp()` RETURNS err so the call FOLDS into the terminal (`_reject(lib.connectorError.stamp(err))`) — the two-line form pushes `_queryEntry.resultSize` past inspector.test.js's fixed-width QI `substring(idx,idx+1800/900)` windows (thin post-fix margins 12/22 — RE-MEASURE on any connector error-branch edit; same fragility class as the #B152 260-char window). Taxonomy, first-match-wins IN ORDER: socket errno (ECONNREFUSED/ECONNRESET/ETIMEDOUT/EPIPE/EHOSTUNREACH/ENETUNREACH/ENETDOWN/EAI_AGAIN transient; **ENOTFOUND EXCLUDED** = DNS misconfig, not a blip) → mysql/sqlite string codes → **node:sqlite NUMERIC `errcode` primary byte** (MEASURED: a busy error is `code:'ERR_SQLITE_ERROR'`+`errcode:5`, NEVER `code:'SQLITE_BUSY'`; `errcode & 0xff` → 5 busy / 6 locked, so extended 261 SQLITE_BUSY_RECOVERY→5; dup-key 1555→19 stays permanent) → pg SQLSTATE (exact set + whole `08` connection class, 5-char-gated) → driver class NAMES (cassandra `NoHostAvailableError`/`OperationTimedOutError`, couchbase `*TimeoutError`/`RequestCanceledError`/`ServiceNotAvailableError`, mongo `MongoNetworkError`/`*SelectionError`) → mongo error LABELS (`TransientTransactionError`/`RetryableWriteError`) → mongo numeric codes (**7 HostNotFound EXCLUDED** = server-side ENOTFOUND; 6 HostUnreachable kept) → couchbase N1QL `cause.first_error_code` 1080 + 12008 bulk-get-retry-exhaustion + `cause.retry===true` (**12009 generic-DML EXCLUDED** = also covers dup-key; the SDK buckets every un-cased 12xxx — permanent 12003 included — into one IndexFailureError grab-bag, so the cause CODE is the signal and that class name must never join the name table; the #B153 envelope-forward reaches classify as a synthesized name-`Error` wrapper WHENEVER THE ENVELOPE CARRIES TEXT, making the code table the SOLE classifier on that path — the 1080 entry is live there, dead only on the raw-SDK shape where the typed timeout name fires first). **#B153 RESIDUAL (fixed 0.5.26)** — the synthesize branch was gated on `typeof(cause.first_error_message) != 'undefined'`, but the node binding marshals `first_error_code`/`first_error_message` onto EVERY query error (the C++ `query_error_context` value-initializes them), so a CLIENT-side SDK timeout — `cause` present, code `0`, message `''` — took the synthesize branch and became `new Error('')`, destroying the typed `Un/AmbiguousTimeoutError` name `classify` matches on, and a retryable timeout was stamped PERMANENT with message `''`. Guard gains `&& cause.first_error_message !== ''` at all three couchbase N1QL `onError` sites, so an empty envelope raw-forwards instead: raw-forwarding is classification-LOSSLESS (the raw error keeps `.cause`, so even a pathological empty-message SERVER envelope still classifies off the code table — the synthesize branch only ever existed to surface envelope text). Named residual: a NON-Error rejection carrying an empty message still loses `.cause` on the `new Error(String(err))` wrap — no known producer (the SDK rejects with typed Errors, and the empty-message case IS the typed timeout). Server-side — NO dist rebuild; restart to pick up. Tests: `couchbase-connector.test.js §08` (14 — source pins on the new clause + client-timeout end-to-end replicas asserting the typed name survives and `classify().isTransient === true`, plus a pre-residual subtract control reproducing the false negative). `classify` is TOTAL — never throws (invoked on the error path; hostile shapes → permanent); `stamp` is frozen-safe. Registered PLAIN `require` (gen-0-captured by the load-once connectors + invoked per-request → a `_require` copy would be a #B32-residual leak candidate). Couchbase is classifiable ONLY because #B153 (`3ac5ea4a`) forwards the RAW driver error its old `.catch` used to discard (and used to HANG on when there was no `cause`). Declared on `GinaLib` in `types/index.d.ts` (the types-runtime-parity two-way gate). **Slice 2 SHIPPED (0.6.2+): opt-in 503 + `Retry-After`.** Per-bundle `server.transientErrors` block — `enabled` (strict boolean true, default false), `retryAfter` (integer seconds 1..86400, default 30), `message` (non-empty string, default = the standard 503 status text). With the opt-in ON, a stamped-transient error resolving to 500 through controller `throwError` renders 503 + `Retry-After: <retryAfter>` (one early guarded `setHeader` rides every egress: the JSON writeHead trio, the HTML fallback page, and the renderCustomError → render pipeline, which forwards `res.getHeaders()` into the HTTP/2 frame), with the user-facing `error` field swapped to `message`; `message`/`stack` body fields keep their existing scope semantics and the incident-ref pairing line keeps full pre-strip detail server-side. Explicit non-500 statuses and permanent errors never upgrade; upgrade applies at BOTH throwError status-resolution sites (the Error/1-arg path and the 2-arg errorObj path). Boot-time shape check warns per malformed key and NEVER refuses boot (a rendering nicety must not gate a boot); the controller-side reader independently normalizes to the same defaults, total (hostile getters → defaults). Deliberately NOT mirrored in the server-side throwError twin — stamped connector errors surface through controller actions only. Wire-verified live on BOTH protocols (isaac engine): h1 503+header, h2 `:status` 503 + `retry-after` (the renderJSON h2-inert-status class does not apply — throwError writes via writeHead; express shares the same engine-agnostic controller path, mechanism-verified only). Deferred: slice 3 (opt-in single auto-retry keyed on the entity OPERATION, not the serving request's HTTP verb — a GET can issue mutating queries). Server-side — NO dist rebuild; restart to pick up. Tests: `test/core/couchbase-connector.test.js §08` (#B153) + `test/lib/connector-error.test.js` (71) + `test/core/transient-errors.test.js` (26: source/order pins both files, real-throwError behaviorals via createTestInstance, extracted-bytes config-normalization matrix, server-twin divergence invariant).
1001
- 280. **Framework control-plane bind host + command-name namespace (0.5.26).** The two control-plane listeners — the command socket (default `8124`, `bin/cmd`, which executes a CLI command per connection) and the MQ listener (default `8125`, `lib/logger/src/containers/mq/listener.js`) — bind an explicit host that defaults to loopback. The `bind_host` setting governs it (env `GINA_BIND_HOST`, flag `gina framework:set --bind-host=`, seeded into `~/.gina/<shortVersion>/settings.json` and carried across shortVersion bumps by the `checkIfSettings` migration map); it is deliberately SEPARATE from `host_v4`, which is the address clients CONNECT to — reusing one key for both would make a workstation whose `host_v4` points at another machine fail to bind its own daemon (EADDRNOTAVAIL). Node binds every interface when `listen()` gets no host argument, and the MQ listener reads `opt.hostV4` specifically, so a caller passing a `host` key leaves it undefined and binds everything — both now default to `127.0.0.1` at the listener itself as well as at the resolution site. Exposing the control plane beyond the local host is a deliberate opt-in, mirroring a bundle's `--http-host`. Separately, `lib/cmd/framework/init.js` `run()` constrains a caller-supplied `topic:action` pair to `/^[A-Za-z0-9_-]+$/` before it becomes a `require()` path (an out-of-namespace segment resolves to `''`, which never exists, so it takes the same "not a valid command" branch); every shipped command group and action matches, including the synthetic `framework:-V` / `framework:--version` forms. An unresolvable name reaching the daemon over the socket ends that client's connection instead of calling `process.exit(1)`, which would stop the daemon serving every other client — an offline one-shot CLI run still exits non-zero. NOTE most command handlers call `process.exit()` themselves, so only `bundle:start` (the sole command `bin/cli` routes online) is designed to run inside the daemon. ⚠️ 0.5.26 shipped a DIAL-side regression with this: CLI-side clients — the command socket dial (`bin/cli`), the MQ speaker and file log containers, and `gina tail` — dialled `host_v4` while the daemon binds `bind_host`, so a co-located CLI could not reach its own daemon whenever `host_v4` was a non-loopback address of the same machine (the common containerized shape: `bundle:start` aborted before the HTTP server ever bound, and `gina tail` printed `[MQTail] ECONNREFUSED <host_v4>:8125`). Fixed post-0.5.26: all four dial sites resolve through `lib/net-locality` — `resolveDialHost(hostV4, bindHost)` dials the bind address (loopback by default, or `bind_host` when it is a concrete non-wildcard address) whenever `host_v4` names one of the machine's own interfaces per `os.networkInterfaces()`, while hostnames, genuinely remote addresses, and enumeration failures dial unchanged (fail-safe) — and the refused-connection error now names the dial target plus the `bind_host` knob. Also fixed in the same change: a `gina framework:set --bind-host=` write now PERSISTS — `bind_host` was the only connection key without a disk-to-env read-back in the settings regeneration (every CLI invocation rewrote it back to env-or-default), and the container bootstrap (`gina-init`) overwrote it unconditionally; both paths now preserve the persisted value, with `GINA_BIND_HOST` still taking precedence and unresolved `${}` template placeholders skipped.
1001
+ 280. **Framework control-plane bind host + command-name namespace (0.5.26).** The two control-plane listeners — the command socket (default `8124`, `bin/cmd`, which executes a CLI command per connection) and the MQ listener (default `8125`, `lib/logger/src/containers/mq/listener.js`) — bind an explicit host that defaults to loopback. The `bind_host` setting governs it (env `GINA_BIND_HOST`, flag `gina framework:set --bind-host=`, seeded into `~/.gina/<shortVersion>/settings.json` and carried across shortVersion bumps by the `checkIfSettings` migration map); it is deliberately SEPARATE from `host_v4`, which is the address clients CONNECT to — reusing one key for both would make a workstation whose `host_v4` points at another machine fail to bind its own daemon (EADDRNOTAVAIL). Node binds every interface when `listen()` gets no host argument, and the MQ listener reads `opt.hostV4` specifically, so a caller passing a `host` key leaves it undefined and binds everything — both now default to `127.0.0.1` at the listener itself as well as at the resolution site. Exposing the control plane beyond the local host is a deliberate opt-in, mirroring a bundle's `--http-host`. Separately, `lib/cmd/framework/init.js` `run()` constrains a caller-supplied `topic:action` pair to `/^[A-Za-z0-9_-]+$/` before it becomes a `require()` path (an out-of-namespace segment resolves to `''`, which never exists, so it takes the same "not a valid command" branch); every shipped command group and action matches, including the synthetic `framework:-V` / `framework:--version` forms. An unresolvable name reaching the daemon over the socket ends that client's connection instead of calling `process.exit(1)`, which would stop the daemon serving every other client — an offline one-shot CLI run still exits non-zero. NOTE most command handlers call `process.exit()` themselves, so only `bundle:start` (the sole command `bin/cli` routes online) is designed to run inside the daemon. ⚠️ 0.5.26 shipped a DIAL-side regression with this: CLI-side clients — the command socket dial (`bin/cli`), the MQ speaker and file log containers, and `gina tail` — dialled `host_v4` while the daemon binds `bind_host`, so a co-located CLI could not reach its own daemon whenever `host_v4` was a non-loopback address of the same machine (the common containerized shape: `bundle:start` aborted before the HTTP server ever bound, and `gina tail` printed `[MQTail] ECONNREFUSED <host_v4>:8125`). Fixed post-0.5.26: all four dial sites resolve through `lib/net-locality` — `resolveDialHost(hostV4, bindHost)` dials the bind address (loopback by default, or `bind_host` when it is a concrete non-wildcard address) whenever `host_v4` names one of the machine's own interfaces per `os.networkInterfaces()`, while hostnames, genuinely remote addresses, and enumeration failures dial unchanged (fail-safe) — and the refused-connection error now names the dial target plus the `bind_host` knob. Also fixed in the same change: a `gina framework:set --bind-host=` write now PERSISTS — `bind_host` was the only connection key without a disk-to-env read-back in the settings regeneration (every CLI invocation rewrote it back to env-or-default), and the container bootstrap (`gina-init`) overwrote it unconditionally; both paths now preserve the persisted value, with `GINA_BIND_HOST` still taking precedence and unresolved `${}` template placeholders skipped. ⚠️ **A THIRD defect on this listener, fixed post-0.6.3 (#B279): its accepted connection sockets carried no `error` handler, so a departed speaker killed the whole command.** Because EVERY `bin/cli` invocation starts the listener on the shared MQ port, commands running concurrently connect to one another as speakers — and when one ends or is killed, the listener still holds its socket in `sessions`. The next write to it (the handshake write, or `self.report()`'s `sessions[sessionId].write()`) raises EPIPE/ECONNRESET **on the connection socket**, which node THROWS when unlistened, dropping the process mid-command: a `bundle:add` died this way after registering its ports but before copying and token-substituting the boilerplate, leaving a half-created bundle whose `index.js` was missing or still carried a literal `${bundle}`. **`server.on('error')` is NOT a substitute — in node every accepted connection is its own EventEmitter**, which is exactly why the pre-existing server-level handler left the hole open (a two-arm probe confirms it never fires for a connection-socket error). The handler is registered BEFORE the handshake write (itself a throwing site) and deletes `sessions[this.sessionId]` — an abruptly-killed peer may never emit `end`, which is what otherwise clears the entry and would leave a dead socket for the next `report()`. Generalisable: a `net.createServer` connection handler that registers `data`/`end` but not `error` is a process-kill waiting for a peer to vanish, and the symptom surfaces far from the socket — as a command that silently did only part of its work.
1002
1002
 
1003
1003
  281. **Production transport posture — `server.requireHttps` / `server.allowInsecure` (#COMPLY9, 0.5.26).** Outside the `local` scope a bundle resolving a cleartext scheme (anything but https — both engines' scheme switches default to the plain server) warns ONCE at boot, naming bundle/scheme/scope and both remediations; `server.requireHttps: true` refuses to boot it instead (the throw lands in init()'s central catch — pre-listen, the cleartext port never binds), and `server.allowInsecure: true` asserts TLS terminates upstream (mesh/ingress/reverse proxy — the documented h2c topology), turning the warn into one info line (same vocabulary as `mcp.json > server > allowInsecure`). Both strict booleans resolved like `scheme` itself (bundle settings win, env.json's `server` block fills via the serverOpt picked-keys merge); a non-boolean-when-present and the both-true contradiction refuse the boot in EVERY scope (the quietly-OFF class). The gate is NOT-local, never is-production — a custom scope (e.g. `beta`) is neither, and a production-keyed gate would silently skip it; `conf.server.scopeIsLocal` derives from def_scope, not the live scope, so it is never consulted. An https:// proxy.json upstream hostname is NAMED in the warn (advisory) but never silences it. Server-side only — no dist rebuild; pickup = restart.
1004
1004
 
@@ -1028,7 +1028,7 @@ Dev-mode query instrumentation captures every database query tied to the current
1028
1028
 
1029
1029
  295. **Inherited-name collisions in query-file loaders warn at boot (#B173, 0.6.1).** Six ORM connectors (mysql, postgresql, sqlite, duckdb, scylladb, mongodb) attach file-derived methods (`sql/<Entity>/<name>.sql`, `cql/…`, `pipelines/<Entity>/<name>.json`) behind `typeof entities[E].prototype[name] !== 'undefined'` — a prototype-CHAIN lookup, so a file named after an inherited member (gina's `Object.prototype` extensions `count()`/`functionCount()`, EventEmitter's `on`/`once`/`emit`, the entity base API) was silently skipped and calls fell through to the inherited member, returning a plausible value of the right type (`count.sql` → the global property counter). Since 0.6.1 the skip logs a `console.warn` through the connector's gina-logger shadow (level `warn`=4 passes the default `log_level: "info"`=6) naming the method, source file path, entity, and a rename hint (`countRows.sql`); own-property collisions still skip SILENTLY by design — user code in the entity `.js` wins over a same-named query file. The skip/attach set is byte-identical to before (the warn sits inside the already-taken branch; behavior verified unchanged live on an isolated boot with a pre-fix control). Couchbase is the deliberate exception: it attaches unconditionally, so there a colliding name SHADOWS the inherited member instead of being skipped (a file named `on.sql` would clobber EventEmitter's `on` for that entity) — and since 0.6.1 that clobber ALSO warns at boot (#B174): an own-member overwrite (a stamped entity property, a previously attached query method) or a non-`Object.prototype` inherited shadow (the EventEmitter API) logs the file path with a rename hint, while shadowing `count()`/`functionCount()` stays silent — there the query file winning is the point. Couchbase attachment behavior itself is unchanged: the warn never skips the file.
1030
1030
 
1031
- 296. **FormValidator submits: one XHR per send + a fail-safe lock release (#B175/#B176, 0.6.1).** `send()` used to reuse ONE module-scope `XMLHttpRequest` (created once at validator init): re-`open()`ing the completed instance synchronously replayed the PREVIOUS submit's still-assigned `onreadystatechange` at readyState 1, re-disabling that form's submit trigger and re-stamping its `data-gina-form-loading` with no release path (the lock was armed at four sites and released only inside readyState 4) — so submitting form B after form A had completed stranded A's trigger natively-disabled until something else happened to re-enable it. Since 0.6.1 every send builds its own LOCAL XHR (per-submit handler/state/lifecycle; the reuse's implicit cross-form abort is gone — each form settles its own request) and registers a `loadend` release — fires on success, error, timeout and abort alike — that removes `disabled`/`aria-disabled` + `data-gina-form-loading` and clears `$form.isSending`/`$form.sent`; `$form.isSending` now genuinely spans send→settled (it used to be cleared at the first readyState transition, i.e. almost immediately — `$form.sent` was the only flag that spanned the request); the timeout path removes `data-gina-form-loading` instead of writing the truthy string `"false"`. Same release, #B176: the live-check opt-out is honored CONSISTENTLY — two validation gates (the select-change handler and the bind-time silent-validation pass) evaluated the rules-count boolean INSIDE `/^(true)$/i.test(…)`, so an explicit `data-gina-form-live-check-enabled="false"` (a truthy string) short-circuited to the count boolean and matched, leaving those two running on a form that had opted out; the gates now test the attribute alone, then check the count. Scope worth knowing before auditing a form's pre-0.6.1 behaviour: the opt-out was NOT ignored outright — the three `registerForLiveChecking` sites (textarea / form-associated custom element / input) already tested the attribute alone, so text as-you-type never ran on an opted-out form, and the submit trigger ended enabled either way because the trigger's show branch is forced whenever live-check is off. The pre-fix behaviour was an inconsistent middle; only bind-time and select-change validation change at pickup. Browser-bundled — consumers re-bake at pickup. Caution that stays by design: `setOptions()` is a MODULE-wide default setter, not per-call — e.g. `setOptions({withRateLimit: false})` turns rate limiting off for every form for the rest of the page lifetime (per-form options are a demand-gated follow-up). **Third release in the same family, #B192 (0.6.3): a REJECTED submit now clears the `isSubmitting` latch.** The submit path arms `instance.$forms[id].isSubmitting = true` before validating — deliberately, so the live-check field listener stays quiet while a request is in flight (it hard-returns on any truthy `isSubmitting`). But the ONLY clear was the XHR settle, which a rejected submit never reaches: it renders the field errors and sends nothing. So one invalid submit attempt latched the flag true for the rest of the page's life — every later keystroke was swallowed, `updateSubmitTriggerState` never ran again, and the submit trigger kept `aria-disabled="true"` + `.gina-form-submit-disabled` (keyboard/AT users hard-blocked; mouse users saw a disabled-looking trigger that still submitted, so presentation and behaviour disagreed). Because the flag lives on the `$forms[id]` OBJECT rather than a listener closure, it also survived a full unbind/rebind — `reBind()` re-gated correctly but never restored the live check, which is the tell that distinguishes this from a listener-set bug. The internal `validate.<id>` handler's invalid branch — the terminal no-send outcome — now releases the latch before its #A11Y1 focus-first-invalid step; the valid branch is untouched, so the latch still spans send→settled and live-check still stays quiet during a real in-flight submit. Diagnostic shortcut for any similar report: read `gina.validator.$forms['<id>'].isSubmitting` — truthy IS the latched state, since that is exactly what the live-check gate tests; the other reliable discriminator is the live-check request count after a keystroke (0 when latched, 1 when healthy). ⚠️ **Do NOT use `isValidating` for this** — an earlier version of this entry did, and a consumer's A/B measured it non-discriminating (2026-08-01). `isValidating` is an in-flight flag, not a latch indicator: it inits `null` and is set true/false by EVERY validation cycle (live-check, focusin, submit), so it reads `null` only until the FIRST cycle completes and `false` at rest forever after — on any form that live-checked before the submit attempt (the normal case) it reads `false` in BOTH the latched and healthy states. The original wording held only because the fixture it was derived from happened never to live-check before submitting. Browser-bundled — consumers re-bake at pickup.
1031
+ 296. **FormValidator submits: one XHR per send + a fail-safe lock release (#B175/#B176, 0.6.1).** `send()` used to reuse ONE module-scope `XMLHttpRequest` (created once at validator init): re-`open()`ing the completed instance synchronously replayed the PREVIOUS submit's still-assigned `onreadystatechange` at readyState 1, re-disabling that form's submit trigger and re-stamping its `data-gina-form-loading` with no release path (the lock was armed at four sites and released only inside readyState 4) — so submitting form B after form A had completed stranded A's trigger natively-disabled until something else happened to re-enable it. Since 0.6.1 every send builds its own LOCAL XHR (per-submit handler/state/lifecycle; the reuse's implicit cross-form abort is gone — each form settles its own request) and registers a `loadend` release — fires on success, error, timeout and abort alike — that removes `disabled`/`aria-disabled` + `data-gina-form-loading` and clears `$form.isSending`/`$form.sent`; `$form.isSending` now genuinely spans send→settled (it used to be cleared at the first readyState transition, i.e. almost immediately — `$form.sent` was the only flag that spanned the request); the timeout path removes `data-gina-form-loading` instead of writing the truthy string `"false"`. Same release, #B176: the live-check opt-out is honored CONSISTENTLY — two validation gates (the select-change handler and the bind-time silent-validation pass) evaluated the rules-count boolean INSIDE `/^(true)$/i.test(…)`, so an explicit `data-gina-form-live-check-enabled="false"` (a truthy string) short-circuited to the count boolean and matched, leaving those two running on a form that had opted out; the gates now test the attribute alone, then check the count. Scope worth knowing before auditing a form's pre-0.6.1 behaviour: the opt-out was NOT ignored outright — the three `registerForLiveChecking` sites (textarea / form-associated custom element / input) already tested the attribute alone, so text as-you-type never ran on an opted-out form, and the submit trigger ended enabled either way because the trigger's show branch is forced whenever live-check is off. The pre-fix behaviour was an inconsistent middle; only bind-time and select-change validation change at pickup. Browser-bundled — consumers re-bake at pickup. Caution that stays by design: `setOptions()` is a MODULE-wide default setter, not per-call — e.g. `setOptions({withRateLimit: false})` turns rate limiting off for every form for the rest of the page lifetime (per-form options are a demand-gated follow-up). **Third release in the same family, #B192 (0.6.3): a REJECTED submit now clears the `isSubmitting` latch.** The submit path arms `instance.$forms[id].isSubmitting = true` before validating — deliberately, so the live-check field listener stays quiet while a request is in flight (it hard-returns on any truthy `isSubmitting`). But the ONLY clear was the XHR settle, which a rejected submit never reaches: it renders the field errors and sends nothing. So one invalid submit attempt latched the flag true for the rest of the page's life — every later keystroke was swallowed, `updateSubmitTriggerState` never ran again, and the submit trigger kept `aria-disabled="true"` + `.gina-form-submit-disabled` (keyboard/AT users hard-blocked; mouse users saw a disabled-looking trigger that still submitted, so presentation and behaviour disagreed). Because the flag lives on the `$forms[id]` OBJECT rather than a listener closure, it also survived a full unbind/rebind — `reBind()` re-gated correctly but never restored the live check, which is the tell that distinguishes this from a listener-set bug. The internal `validate.<id>` handler's invalid branch — the terminal no-send outcome — now releases the latch before its #A11Y1 focus-first-invalid step; the valid branch is untouched, so the latch still spans send→settled and live-check still stays quiet during a real in-flight submit. Diagnostic shortcut for any similar report: read `gina.validator.$forms['<id>'].isSubmitting` — truthy IS the latched state, since that is exactly what the live-check gate tests; the other reliable discriminator is the live-check request count after a keystroke (0 when latched, 1 when healthy). ⚠️ **Do NOT use `isValidating` for this** — an earlier version of this entry did, and a consumer's A/B measured it non-discriminating (2026-08-01). `isValidating` is an in-flight flag, not a latch indicator: it inits `null` and is set true/false by EVERY validation cycle (live-check, focusin, submit), so it reads `null` only until the FIRST cycle completes and `false` at rest forever after — on any form that live-checked before the submit attempt (the normal case) it reads `false` in BOTH the latched and healthy states. The original wording held only because the fixture it was derived from happened never to live-check before submitting. Browser-bundled — consumers re-bake at pickup. **Fourth release in the same family (0.6.x): a trigger-scoped loading state the framework owns end to end.** `data-gina-form-loading` is FORM-scoped and, per the release above, is removed rather than set false — it says "this form has a request in flight" and nothing about which control started it. The new `data-gina-loading` is TRIGGER-scoped: the submit-like element the user actually operated carries `"true"` while its action runs and `"false"` the moment that action reaches ANY terminal outcome. Five paths release it, and two of them reach no XHR at all: a submit REJECTED by validation (the motivating defect — it renders errors and sends nothing, so no XHR lifecycle exists to release anything, and only the framework knows a submit was refused before it started), `send()`'s rate-limit early return (the same no-request shape, reached by a double submit), plus `loadend` (success, error, timeout and abort alike), readyState 4, and — by construction — never arming at all when the click hits a trigger the disabled-trigger gate turns away. ⚠️ The `"false"` value is deliberate and it CONSTRAINS READERS: unlike `data-gina-form-loading`, the attribute stays PRESENT when released, so match on the VALUE — `[data-gina-loading="true"]`, never a bare `[data-gina-loading]`, which matches a released trigger and pins the loading style on permanently. Framework code must not presence-check it either; the primitive exposes an `isArmed()` that compares `=== "true"`. Arming happens at two sites because one is not enough: the click proxy (after the disabled-trigger gate, climbing from `event.target` to the owning `button`/`a`, since a click on a wrapped label such as `<button type="submit"><span>Save</span></button>` targets the inner node), and the native submit proxy, which is where Enter-key submits, `form.submit()`, `$validator.submit()` AND that same wrapped-label click actually surface — the span has no `.type`, so the click proxy's submit branch never fires for it. The attribute NAME is configurable for projects already built on a different convention: `gina.setOptions({ loadingAttribute: 'data-loading' })` or the `data-gina-config` script-tag attribute. Read LAZILY on every call (`gina.config` is populated after the bundle loads, so a value captured at definition time is always the default), defaulting to `data-gina-loading`. There is deliberately NO auto-detection: the attribute only exists while something is loading, so a DOM probe reads nothing at rest, a stylesheet probe races late-loaded sheets and throws on cross-origin ones, and writing BOTH names stacks a consumer's spinner on top of the framework's. The logic lives in a shared `lib/loading-state` primitive (stateless: resolve the name, write the two values, climb to the owning trigger — the caller decides what is armed and remembers it), aliased into the browser bundle in BOTH build configs and required from `core.js`; it is intentionally NOT in the `lib/index.js` registry, since nothing server-side uses it. Browser-bundled — consumers re-bake at pickup. **`resolveTrigger` takes an optional third argument, a predicate saying what counts as a trigger** (0.6.4): the default recognises submit-like controls only (`button`/`input[type=submit]`, `a[data-gina-form-submit]`), so a plugin arming something else — link's `a[data-gina-link]`, popin's `a[data-gina-dialog]` — passes its own, or the climb finds nothing and returns the ORIGINAL clicked node, arming an inner `<span>` instead of the anchor. The predicate belongs to the caller because this primitive owns the WALK while each plugin owns its notion of a trigger; a predicate that throws is deliberately not caught. **The same one-XHR-per-request repair has now been applied to the LINK plugin (0.6.4).** link built ONE transport in its `init` handler and reused it for every click, which is worse than a race: `open()` on an object still carrying a request implicitly ABORTS it, an aborted request reaches readyState 4 with **status 0**, and `handleXhr`'s readyState-4 chain has no branch for status 0 (`} else if ( xhr.status != 0)`) — so the first click's completion fired NOTHING, neither success nor error. Each link request now builds its own transport and carries a sequence taken **before** the supersede-abort, which makes an abort stale by construction; the guard wraps the handler `handleXhr` installed rather than teaching that shared helper about link's sequence. ⚠️ **Still open and deliberately so:** a genuine network failure remains silent and a hanging link request still has no deadline (`xhr.timeout` is never set, so `events.js`'s already-written `ontimeout` is unreachable) — both are consumer-visible changes held back for their own release note. Consequence for anyone arming a non-form trigger: release on a **`loadend` listener**, which fires for success, error, abort and status-0 alike and emits no events of its own; only an indefinite hang escapes it. **link now does exactly that (0.6.4):** `linkRequest` arms `data-gina-loading` on the anchor and releases it from a `loadend` listener. It arms THERE, not in the click handlers, because every entry funnels through it (direct click, the child-delegation proxy — which dispatches its custom event ON the anchor, so the handler never sees the inner node — and the public `gina.link.request()`), which is also why link passes no trigger predicate: `$el` is already the anchor, so there is nothing to walk up to. Two invariants that are easy to invert: the arm is GATED on `xhr.addEventListener` existing — never arm what cannot be released, since the CORS branch can still swap in a legacy `XDomainRequest` — and the release is deliberately NOT sequence-guarded, because the sequence exists to drop a stale RESPONSE whereas a superseded request must still RELEASE its trigger; guarding it would strand precisely the trigger the feature protects. That ordering is safe by construction rather than by luck, and it was measured, not assumed: `abort()` fires `abort` + `loadend` SYNCHRONOUSLY, before `abort()` returns, so the superseded request releases before the newer click arms. A synchronous throw out of `send()` releases and rethrows unchanged. The indefinite-hang residual above is the one outcome still uncovered. **Default CSS now ships with it (0.6.4).** `core/asset/plugin/src/vendor/gina/loading-state/sass/loading-state.scss` gives `[data-gina-loading="true"]` a `progress` cursor plus an opacity pulse gated on `prefers-reduced-motion: no-preference`, with a static dim under `reduce` so reduced-motion users get a traded signal rather than none. Two omissions are deliberate and should not be "fixed": NO pseudo-element spinner (it would stack with a spinner the consumer already ships — measured — and would force `position: relative` onto a trigger the framework does not own) and NO `pointer-events: none` (it would suppress the second click link deliberately accepts and supersedes). Specificity is one attribute (0,1,0) so any consumer class wins outright, `animation: none` drops just the motion, and renaming through `loadingAttribute` opts out of the stylesheet entirely because it is keyed on the default name. ⚠️ **Build gotcha, general to any new gina stylesheet:** the CSS phase discovers stylesheets by scanning `src/vendor/gina/<name>/sass/` and concatenates ONLY the compiled file whose basename matches its own directory — so `foo/sass/bar.scss` compiles to `foo/css/bar.css` and then never reaches `gina.min.css`. `inspector` is skipped entirely (served separately). The intermediate `src/.../css/*.css` IS tracked (popin's are), while its `.css.map` sibling is gitignored. **POPIN arms its trigger too (0.6.4).** `armPopinTrigger()` runs at both arm sites; releases at readyState 4, in `popinClose`, and in `popinUnbind` — the last is load-bearing because routing transitions reach `popinUnbind` WITHOUT `popinClose`, so a route-torn-down popin would otherwise leave its trigger marked forever. ⚠️ **`data-gina-popin-loading` and `data-gina-loading` are NOT the same signal and a rename between them is a REGRESSION:** the first is on `$popin.target` (the CONTAINER, "this popin is filling"), the second on the TRIGGER ("this control is busy"). ⚠️ **Neither popin NOR link needs a `resolveTrigger` predicate** — both resolve by id/`closest()`, never from a raw click target (popin: `e.currentTarget.id` at the per-element listener, `event.target.closest('[data-gina-dialog],…')` at the delegated one, ids assigned when missing), so the predicate added for them has no consumer. ⚠️ **The helper is scoped to the TRIGGER on purpose:** `popin.test.js:778` pins `showLoadingShell($popin, $el);` at >=2 LITERAL occurrences, so absorbing the whole arm block into a helper reds it — widening that helper needs approval for the pin first. (Grep it with `-F`: `$` and `(` are PCRE metacharacters, so a plain `grep -c` reads a false 0.) **Still open — `#B285`:** `consumePreload` short-circuits both arm sites, so a popin opened from an IN-FLIGHT preload shows no affordance; it cannot strand (nothing armed), and any arm written INSIDE `consumePreload` reds four tests because `popin.test.js`'s `driveConsume` extract-and-executes those bytes with only 7 injected symbols — neither `document` nor `loadingState`.
1032
1032
 
1033
1033
  297. **Validator error labels & rendering — observable keys now translatable, announcements separator-joined, identical messages deduped (#B178 family, 0.6.1).** The key an app can OBSERVE in a field's `errors` object was not always the key the label catalog is consulted under: four rule families write the error under a generic key while consulting a SPECIFIC label key — the float coercion's NaN branch renders `errorLabels['toFloatNAN']` into `errors['toFloat']`, and the number/integer/string Length families render the `Min`/`Max` label variants into the generic `is<X>Length` errors key — so translating the observable key (bundle catalog `_validator` node or `setErrorLabels()`) was a silent no-op and a partial catalog rendered one localized message beside one English default. Fixed with an alias fill applied to every app-supplied label layer (server catalog node, client catalog whisper, per-culture registrations, `setErrorLabels()`): a supplied generic fills the specific keys the app did not supply itself; a supplied specific still wins; English defaults are untouched. Same release: numbered `is` aliases (`is1`, `is2`, …) had no per-alias default label, so a failing alias with no rule-supplied text rendered an EMPTY message — they now fall back to the shared `is` label (translate catalog key `is` once to cover every alias); and the user-validator setup loop unconditionally reset every user-defined validator's label to the English default, clobbering catalog translations — it now fills only when the app supplied none. Two render-side fixes ride along: the error container renders one `<p>` per error key and the aria-live announcement passed the container's raw `textContent`, which concatenates with NO separator — a screen reader received multiple messages as one run-on string (visually the messages stack as blocks; only the announcement channel ran them together); announcements now join the per-message texts with a sentence separator. And two rules resolving to byte-identical message text (canonically a coercion paired with its validator, both failing on the same non-numeric input) rendered the same sentence twice — each distinct text now renders once, while the dev inspector still records every error key. Browser-bundled — consumers re-bake at pickup; apps that translated only observable keys will see those messages switch from English to their language.
1034
1034
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gina",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "MVC framework for Node.js and Bun with built-in HTTP/2, multi-bundle architecture, and scope-based data isolation — no Express dependency",
5
5
  "keywords": [
6
6
  "nodejs",
@@ -76,12 +76,12 @@
76
76
  "gina-container": "bin/gina-container",
77
77
  "gina-init": "bin/gina-init"
78
78
  },
79
- "main": "./framework/v0.6.3/core/gna",
79
+ "main": "./framework/v0.6.4/core/gna",
80
80
  "exports": {
81
81
  ".": {
82
82
  "types": "./types/index.d.ts",
83
83
  "import": "./index.mjs",
84
- "require": "./framework/v0.6.3/core/gna.js"
84
+ "require": "./framework/v0.6.4/core/gna.js"
85
85
  },
86
86
  "./gna": {
87
87
  "types": "./types/gna.d.ts",
@@ -183,6 +183,20 @@
183
183
  }
184
184
  }
185
185
  },
186
+ "secrets": {
187
+ "type": "object",
188
+ "description": "Where `${secret:KEY}` placeholders in this bundle's JSON configs are resolved from. Omit the block entirely and resolution behaves exactly as it always has: the framework environment first, then process.env, failing closed when a key is unset or empty. Declaring it adds a lower-precedence file tier; it never removes or reorders the environment tiers above it.",
189
+ "additionalProperties": false,
190
+ "properties": {
191
+ "file": {
192
+ "type": ["string", "array", "null"],
193
+ "items": { "type": "string", "minLength": 1 },
194
+ "minLength": 1,
195
+ "description": "One `.env`-style file path, or an array of them, layered UNDERNEATH the environment. Written with the ordinary config tokens, e.g. [\"${homedir}/secrets.env\", \"${homedir}/${scope}/secrets.env\"]. A NON-EMPTY environment value always wins: every production delivery mechanism (a Kubernetes envFrom secretRef, an ECS task secret, `sops exec-env`, a CI export) arrives through the environment, so a file that won could let a stale plaintext copy baked into an image silently shadow the credential the platform injected while the bundle looked healthy. As a fallback it can only ever fill a gap. A variable that is SET but EMPTY counts as absent, so the file tier fills it — that is what keeps the common `environment: [\"X=${X}\"]` passthrough working when the outer variable is unset — but because the same shape also arises when an entrypoint's `export X=\"$(fetch …)\"` fails, the fallback emits a warning naming the key so the dangerous case is visible without the benign one becoming fatal. Within the array, LATER entries win over earlier ones, so a shared base file and a per-scope file combine. A declared file that does not exist contributes nothing and is not an error — a project may ship a base file and add the per-scope one only on some targets. A declared file that EXISTS but cannot be read (wrong permissions or ownership) IS an error and boot refuses, naming the path: skipping it silently would drop the chain onto the shared base file and run the bundle on the wrong credential with no signal. Resolution stays fail-closed when a key is in neither tier. Boot refuses on a non-string or empty entry, on a `${secret:…}` placeholder (the backend that would resolve it is the one being built), and on any `${…}` token the substitution pass did not know — unknown tokens are preserved verbatim by design, so a typo would otherwise become a silent lookup for a literally-named file that never exists. NOT a secrets-management integration: it does not decrypt, and pointed at ciphertext it yields ciphertext silently. For SOPS, Vault or a KMS keep decrypting at the container entrypoint so values land in the environment, which this tier already prefers. Files are layered once per config-load cycle, so editing one after the bundle has started changes nothing until a restart.",
196
+ "default": null
197
+ }
198
+ }
199
+ },
186
200
  "server": {
187
201
  "type": "object",
188
202
  "description": "HTTP server engine and protocol configuration.",
@@ -1 +0,0 @@
1
- 0.6.3
@@ -1 +0,0 @@
1
- @keyframes gina-popin-skeleton-shimmer{0%{background-position:200%0}to{background-position:-200%0}}@font-face{font-family:"have_heart_oneregular";src:url(fonts/have_heart_one-webfont.woff2)format("woff2"),url(fonts/have_heart_one-webfont.woff)format("woff");font-weight:400;font-style:normal}.gina-popin-container{background:#fff}.gina-popins-overlay,div.gina-popin-container{visibility:hidden;opacity:0;z-index:1;transition:opacity .3s,visibility 0 .3s}.gina-popins-overlay.gina-popin-is-active,div.gina-popin-container.gina-popin-is-active{opacity:1;visibility:visible;transition-delay:0}dialog.gina-popin-container{padding:0;border:0;z-index:1}@media (prefers-reduced-motion:no-preference){dialog.gina-popin-container{opacity:0;transform:translateY(-.5rem);transition:opacity .2s ease,transform .2s ease,overlay .2s ease allow-discrete,display .2s ease allow-discrete}dialog.gina-popin-container[open]{opacity:1;transform:translateY(0)}@starting-style{dialog.gina-popin-container[open]{opacity:0;transform:translateY(-.5rem)}}dialog.gina-popin-container::backdrop{opacity:0;transition:opacity .2s ease,overlay .2s ease allow-discrete,display .2s ease allow-discrete}dialog.gina-popin-container[open]::backdrop{opacity:1}@starting-style{dialog.gina-popin-container[open]::backdrop{opacity:0}}}body[data-gina-popin-scroll-lock]{overflow:hidden}.gina-popins-overlay{position:fixed;left:0;top:0;display:grid;place-items:center;width:100%;height:100%;transition:opacity .3s,visibility .3s;overflow:auto;background:rgba(0,0,0,.75)}.gina-popins-overlay.gina-popin-is-active{backdrop-filter:blur(6px)}dialog::backdrop{background-color:rgba(0,0,0,.75);backdrop-filter:blur(6px)}.gina-popin-skeleton{padding:1.5rem;min-width:18rem;max-width:90vw}.gina-popin-skeleton .gina-popin-skeleton-line{height:.85rem;margin-bottom:.75rem;border-radius:4px;background:linear-gradient(90deg,#ededed 25%,#f7f7f7 50%,#ededed 75%);background-size:200% 100%;animation:gina-popin-skeleton-shimmer 1.4s ease-in-out infinite}.gina-popin-skeleton .gina-popin-skeleton-line:last-child{margin-bottom:0}.gina-popin-skeleton .gina-popin-skeleton-title{height:1.4rem;width:60%;margin-bottom:1.25rem}.gina-popin-skeleton .gina-popin-skeleton-line--short{width:40%}@media (prefers-reduced-motion:reduce){.gina-popin-skeleton .gina-popin-skeleton-line{animation:none}}