gina 0.6.20 → 0.6.21

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 (593) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +18 -20
  3. package/ROADMAP.md +1 -0
  4. package/framework/v0.6.21/VERSION +1 -0
  5. package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/js/gina.js +101 -10
  6. package/framework/v0.6.21/core/asset/plugin/dist/vendor/gina/js/gina.min.js +653 -0
  7. package/framework/v0.6.21/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  8. package/framework/v0.6.21/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  9. package/framework/{v0.6.20 → v0.6.21}/core/model/entity.js +1 -1
  10. package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/validator/src/main.js +15 -2
  11. package/framework/{v0.6.20 → v0.6.21}/lib/merge/src/main.js +24 -0
  12. package/framework/{v0.6.20 → v0.6.21}/lib/routing/src/main.js +56 -8
  13. package/framework/{v0.6.20 → v0.6.21}/package.json +1 -1
  14. package/gna.js +4 -4
  15. package/llms.txt +3 -1
  16. package/package.json +4 -4
  17. package/framework/v0.6.20/VERSION +0 -1
  18. package/framework/v0.6.20/core/asset/plugin/dist/vendor/gina/js/gina.min.js +0 -652
  19. package/framework/v0.6.20/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  20. package/framework/v0.6.20/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  21. /package/framework/{v0.6.20 → v0.6.21}/AUTHORS +0 -0
  22. /package/framework/{v0.6.20 → v0.6.21}/LICENSE +0 -0
  23. /package/framework/{v0.6.20 → v0.6.21}/core/asset/html/nolayout.html +0 -0
  24. /package/framework/{v0.6.20 → v0.6.21}/core/asset/html/static.html +0 -0
  25. /package/framework/{v0.6.20 → v0.6.21}/core/asset/img/android-chrome-192x192.png +0 -0
  26. /package/framework/{v0.6.20 → v0.6.21}/core/asset/img/android-chrome-512x512.png +0 -0
  27. /package/framework/{v0.6.20 → v0.6.21}/core/asset/img/apple-touch-icon.png +0 -0
  28. /package/framework/{v0.6.20 → v0.6.21}/core/asset/img/favicon-16x16.png +0 -0
  29. /package/framework/{v0.6.20 → v0.6.21}/core/asset/img/favicon-32x32.png +0 -0
  30. /package/framework/{v0.6.20 → v0.6.21}/core/asset/img/favicon.ico +0 -0
  31. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/README.md +0 -0
  32. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
  33. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
  34. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
  35. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -0
  36. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
  37. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
  38. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +0 -0
  39. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
  40. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
  41. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
  42. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/inspector/index.html +0 -0
  43. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +0 -0
  44. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +0 -0
  45. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
  46. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
  47. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
  48. /package/framework/{v0.6.20 → v0.6.21}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
  49. /package/framework/{v0.6.20 → v0.6.21}/core/config.js +0 -0
  50. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/ai/index.js +0 -0
  51. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/ai/lib/connector.js +0 -0
  52. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/index.js +0 -0
  53. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/connector.js +0 -0
  54. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/connector.v3.js +0 -0
  55. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/connector.v4.js +0 -0
  56. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/n1ql.js +0 -0
  57. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/session-store.js +0 -0
  58. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/session-store.v3.js +0 -0
  59. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/session-store.v4.js +0 -0
  60. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/couchbase/lib/storage-store.js +0 -0
  61. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/duckdb/index.js +0 -0
  62. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/duckdb/lib/connector.js +0 -0
  63. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mongodb/index.js +0 -0
  64. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mongodb/lib/connector.js +0 -0
  65. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mongodb/lib/job-store.js +0 -0
  66. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
  67. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mongodb/lib/session-store.js +0 -0
  68. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mysql/index.js +0 -0
  69. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/mysql/lib/connector.js +0 -0
  70. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/param-redact.js +0 -0
  71. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/postgresql/index.js +0 -0
  72. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/postgresql/lib/connector.js +0 -0
  73. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/redis/index.js +0 -0
  74. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/redis/lib/connector.js +0 -0
  75. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/redis/lib/job-store.js +0 -0
  76. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/redis/lib/kv-store.js +0 -0
  77. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/redis/lib/render-cache-store.js +0 -0
  78. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/redis/lib/session-store.js +0 -0
  79. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/scylladb/index.js +0 -0
  80. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/scylladb/lib/connector.js +0 -0
  81. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/scylladb/lib/session-store.js +0 -0
  82. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/settle-once.js +0 -0
  83. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/sql-parser.js +0 -0
  84. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/sqlite/index.js +0 -0
  85. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/sqlite/lib/connector.js +0 -0
  86. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/sqlite/lib/job-store.js +0 -0
  87. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/sqlite/lib/kv-store.js +0 -0
  88. /package/framework/{v0.6.20 → v0.6.21}/core/connectors/sqlite/lib/session-store.js +0 -0
  89. /package/framework/{v0.6.20 → v0.6.21}/core/content.encoding +0 -0
  90. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.framework.js +0 -0
  91. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.js +0 -0
  92. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-json.js +0 -0
  93. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-nunjucks-async.js +0 -0
  94. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-nunjucks.js +0 -0
  95. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-stream.js +0 -0
  96. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-swig-async.js +0 -0
  97. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-swig.js +0 -0
  98. /package/framework/{v0.6.20 → v0.6.21}/core/controller/controller.render-v1.js +0 -0
  99. /package/framework/{v0.6.20 → v0.6.21}/core/controller/index.js +0 -0
  100. /package/framework/{v0.6.20 → v0.6.21}/core/controller/inspector-window-emit.js +0 -0
  101. /package/framework/{v0.6.20 → v0.6.21}/core/controller/release-banner.js +0 -0
  102. /package/framework/{v0.6.20 → v0.6.21}/core/dev/index.js +0 -0
  103. /package/framework/{v0.6.20 → v0.6.21}/core/dev/lib/class.js +0 -0
  104. /package/framework/{v0.6.20 → v0.6.21}/core/dev/lib/factory.js +0 -0
  105. /package/framework/{v0.6.20 → v0.6.21}/core/dev/lib/tools.js +0 -0
  106. /package/framework/{v0.6.20 → v0.6.21}/core/gna.js +0 -0
  107. /package/framework/{v0.6.20 → v0.6.21}/core/locales/README.md +0 -0
  108. /package/framework/{v0.6.20 → v0.6.21}/core/locales/currency.json +0 -0
  109. /package/framework/{v0.6.20 → v0.6.21}/core/locales/dist/language/en.json +0 -0
  110. /package/framework/{v0.6.20 → v0.6.21}/core/locales/dist/language/fr.json +0 -0
  111. /package/framework/{v0.6.20 → v0.6.21}/core/locales/dist/region/en.json +0 -0
  112. /package/framework/{v0.6.20 → v0.6.21}/core/locales/dist/region/fr.json +0 -0
  113. /package/framework/{v0.6.20 → v0.6.21}/core/locales/index.js +0 -0
  114. /package/framework/{v0.6.20 → v0.6.21}/core/mime.types +0 -0
  115. /package/framework/{v0.6.20 → v0.6.21}/core/model/index.js +0 -0
  116. /package/framework/{v0.6.20 → v0.6.21}/core/model/template/entityFactory.js +0 -0
  117. /package/framework/{v0.6.20 → v0.6.21}/core/model/template/index.js +0 -0
  118. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/README.md +0 -0
  119. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/index.js +0 -0
  120. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/csrf/README.md +0 -0
  121. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/csrf/package.json +0 -0
  122. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/csrf/src/main.js +0 -0
  123. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/README.md +0 -0
  124. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/coep/README.md +0 -0
  125. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/coep/package.json +0 -0
  126. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
  127. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/coop/README.md +0 -0
  128. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/coop/package.json +0 -0
  129. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
  130. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/corp/README.md +0 -0
  131. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/corp/package.json +0 -0
  132. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
  133. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/csp/README.md +0 -0
  134. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/csp/package.json +0 -0
  135. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
  136. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/hide-powered-by/README.md +0 -0
  137. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
  138. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
  139. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/hsts/README.md +0 -0
  140. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/hsts/package.json +0 -0
  141. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
  142. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
  143. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
  144. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
  145. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/package.json +0 -0
  146. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
  147. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
  148. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
  149. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/src/main.js +0 -0
  150. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
  151. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
  152. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
  153. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
  154. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
  155. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
  156. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
  157. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
  158. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
  159. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
  160. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
  161. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
  162. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
  163. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
  164. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
  165. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
  166. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
  167. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
  168. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/session/README.md +0 -0
  169. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/session/package.json +0 -0
  170. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/session/src/main.js +0 -0
  171. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/storage/README.md +0 -0
  172. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/storage/build.json +0 -0
  173. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/storage/package.json +0 -0
  174. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/storage/src/main.js +0 -0
  175. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/validator/README.md +0 -0
  176. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/validator/build.json +0 -0
  177. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/validator/package.json +0 -0
  178. /package/framework/{v0.6.20 → v0.6.21}/core/plugins/lib/validator/src/form-validator.js +0 -0
  179. /package/framework/{v0.6.20 → v0.6.21}/core/router.js +0 -0
  180. /package/framework/{v0.6.20 → v0.6.21}/core/server.express.js +0 -0
  181. /package/framework/{v0.6.20 → v0.6.21}/core/server.isaac.js +0 -0
  182. /package/framework/{v0.6.20 → v0.6.21}/core/server.js +0 -0
  183. /package/framework/{v0.6.20 → v0.6.21}/core/status.codes +0 -0
  184. /package/framework/{v0.6.20 → v0.6.21}/core/template/_gitignore +0 -0
  185. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/app.json +0 -0
  186. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/connectors.json +0 -0
  187. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/routing.json +0 -0
  188. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/settings.json +0 -0
  189. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
  190. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/templates.json +0 -0
  191. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/config/watchers.json +0 -0
  192. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
  193. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
  194. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
  195. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/index.js +0 -0
  196. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle/locales/en.json +0 -0
  197. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
  198. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/css/default.css +0 -0
  199. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/css/home.css +0 -0
  200. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
  201. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
  202. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
  203. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
  204. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
  205. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/readme.md +0 -0
  206. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_public/sw.js +0 -0
  207. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
  208. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
  209. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
  210. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
  211. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
  212. /package/framework/{v0.6.20 → v0.6.21}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
  213. /package/framework/{v0.6.20 → v0.6.21}/core/template/command/gina.bat.tpl +0 -0
  214. /package/framework/{v0.6.20 → v0.6.21}/core/template/command/gina.tpl +0 -0
  215. /package/framework/{v0.6.20 → v0.6.21}/core/template/conf/env.json +0 -0
  216. /package/framework/{v0.6.20 → v0.6.21}/core/template/conf/manifest.json +0 -0
  217. /package/framework/{v0.6.20 → v0.6.21}/core/template/conf/package.json +0 -0
  218. /package/framework/{v0.6.20 → v0.6.21}/core/template/conf/settings.json +0 -0
  219. /package/framework/{v0.6.20 → v0.6.21}/core/template/conf/statics.json +0 -0
  220. /package/framework/{v0.6.20 → v0.6.21}/core/template/conf/templates.json +0 -0
  221. /package/framework/{v0.6.20 → v0.6.21}/core/template/error/client/json/401.json +0 -0
  222. /package/framework/{v0.6.20 → v0.6.21}/core/template/error/client/json/403.json +0 -0
  223. /package/framework/{v0.6.20 → v0.6.21}/core/template/error/client/json/404.json +0 -0
  224. /package/framework/{v0.6.20 → v0.6.21}/core/template/error/server/html/50x.html +0 -0
  225. /package/framework/{v0.6.20 → v0.6.21}/core/template/error/server/json/500.json +0 -0
  226. /package/framework/{v0.6.20 → v0.6.21}/core/template/error/server/json/503.json +0 -0
  227. /package/framework/{v0.6.20 → v0.6.21}/core/template/extensions/logger/config.json +0 -0
  228. /package/framework/{v0.6.20 → v0.6.21}/helpers/console.js +0 -0
  229. /package/framework/{v0.6.20 → v0.6.21}/helpers/context.js +0 -0
  230. /package/framework/{v0.6.20 → v0.6.21}/helpers/data/LICENSE +0 -0
  231. /package/framework/{v0.6.20 → v0.6.21}/helpers/data/README.md +0 -0
  232. /package/framework/{v0.6.20 → v0.6.21}/helpers/data/package.json +0 -0
  233. /package/framework/{v0.6.20 → v0.6.21}/helpers/data/src/main.js +0 -0
  234. /package/framework/{v0.6.20 → v0.6.21}/helpers/dateFormat.js +0 -0
  235. /package/framework/{v0.6.20 → v0.6.21}/helpers/index.js +0 -0
  236. /package/framework/{v0.6.20 → v0.6.21}/helpers/json/LICENSE +0 -0
  237. /package/framework/{v0.6.20 → v0.6.21}/helpers/json/README.md +0 -0
  238. /package/framework/{v0.6.20 → v0.6.21}/helpers/json/package.json +0 -0
  239. /package/framework/{v0.6.20 → v0.6.21}/helpers/json/src/main.js +0 -0
  240. /package/framework/{v0.6.20 → v0.6.21}/helpers/path.js +0 -0
  241. /package/framework/{v0.6.20 → v0.6.21}/helpers/plugins/README.md +0 -0
  242. /package/framework/{v0.6.20 → v0.6.21}/helpers/plugins/package.json +0 -0
  243. /package/framework/{v0.6.20 → v0.6.21}/helpers/plugins/src/api-error.js +0 -0
  244. /package/framework/{v0.6.20 → v0.6.21}/helpers/plugins/src/main.js +0 -0
  245. /package/framework/{v0.6.20 → v0.6.21}/helpers/prototypes.js +0 -0
  246. /package/framework/{v0.6.20 → v0.6.21}/helpers/task.js +0 -0
  247. /package/framework/{v0.6.20 → v0.6.21}/helpers/text.js +0 -0
  248. /package/framework/{v0.6.20 → v0.6.21}/lib/admin/package.json +0 -0
  249. /package/framework/{v0.6.20 → v0.6.21}/lib/admin/src/main.js +0 -0
  250. /package/framework/{v0.6.20 → v0.6.21}/lib/archiver/README.md +0 -0
  251. /package/framework/{v0.6.20 → v0.6.21}/lib/archiver/build.json +0 -0
  252. /package/framework/{v0.6.20 → v0.6.21}/lib/archiver/package.json +0 -0
  253. /package/framework/{v0.6.20 → v0.6.21}/lib/archiver/src/dep/jszip.min.js +0 -0
  254. /package/framework/{v0.6.20 → v0.6.21}/lib/archiver/src/main.js +0 -0
  255. /package/framework/{v0.6.20 → v0.6.21}/lib/async/package.json +0 -0
  256. /package/framework/{v0.6.20 → v0.6.21}/lib/async/src/main.js +0 -0
  257. /package/framework/{v0.6.20 → v0.6.21}/lib/audit/package.json +0 -0
  258. /package/framework/{v0.6.20 → v0.6.21}/lib/audit/src/main.js +0 -0
  259. /package/framework/{v0.6.20 → v0.6.21}/lib/audit-store.js +0 -0
  260. /package/framework/{v0.6.20 → v0.6.21}/lib/authn/package.json +0 -0
  261. /package/framework/{v0.6.20 → v0.6.21}/lib/authn/src/lockout.js +0 -0
  262. /package/framework/{v0.6.20 → v0.6.21}/lib/authn/src/main.js +0 -0
  263. /package/framework/{v0.6.20 → v0.6.21}/lib/authn/src/totp.js +0 -0
  264. /package/framework/{v0.6.20 → v0.6.21}/lib/authz-gate/package.json +0 -0
  265. /package/framework/{v0.6.20 → v0.6.21}/lib/authz-gate/src/main.js +0 -0
  266. /package/framework/{v0.6.20 → v0.6.21}/lib/cache/README.md +0 -0
  267. /package/framework/{v0.6.20 → v0.6.21}/lib/cache/build.json +0 -0
  268. /package/framework/{v0.6.20 → v0.6.21}/lib/cache/package.json +0 -0
  269. /package/framework/{v0.6.20 → v0.6.21}/lib/cache/src/main.js +0 -0
  270. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/aliases.json +0 -0
  271. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/audit/arguments.json +0 -0
  272. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/audit/help.txt +0 -0
  273. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/audit/verify.js +0 -0
  274. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/add.js +0 -0
  275. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/arguments.json +0 -0
  276. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/build.js +0 -0
  277. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/copy.js +0 -0
  278. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/cp.js +0 -0
  279. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/help.js +0 -0
  280. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/help.txt +0 -0
  281. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
  282. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/list.js +0 -0
  283. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/man.js +0 -0
  284. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/mcp-start.js +0 -0
  285. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/mcp.js +0 -0
  286. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/oas.js +0 -0
  287. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/openapi.js +0 -0
  288. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/remove.js +0 -0
  289. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/rename.js +0 -0
  290. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/restart.js +0 -0
  291. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/rm.js +0 -0
  292. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/start.js +0 -0
  293. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/status.js +0 -0
  294. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/stop.js +0 -0
  295. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/bundle/types.js +0 -0
  296. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/cache/arguments.json +0 -0
  297. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/cache/clear.js +0 -0
  298. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/cache/help.txt +0 -0
  299. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/cache/stats.js +0 -0
  300. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/add.js +0 -0
  301. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/arguments.json +0 -0
  302. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/help.js +0 -0
  303. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/help.txt +0 -0
  304. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/infer.js +0 -0
  305. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/list.js +0 -0
  306. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/migrate.js +0 -0
  307. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/models.js +0 -0
  308. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/remove.js +0 -0
  309. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/rm.js +0 -0
  310. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/connector/test.js +0 -0
  311. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/container/arguments.json +0 -0
  312. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/container/help.js +0 -0
  313. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/container/help.txt +0 -0
  314. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/container/man.js +0 -0
  315. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/container/ps.js +0 -0
  316. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/container/stop.js +0 -0
  317. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/add.js +0 -0
  318. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/arguments.json +0 -0
  319. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/help.txt +0 -0
  320. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/inc/args.js +0 -0
  321. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/inc/namespace.js +0 -0
  322. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
  323. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/inc/reference-scan.js +0 -0
  324. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/inc/scaffold.js +0 -0
  325. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/remove.js +0 -0
  326. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/rename.js +0 -0
  327. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/controller/rm.js +0 -0
  328. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/add.js +0 -0
  329. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/get.js +0 -0
  330. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/help.js +0 -0
  331. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/help.txt +0 -0
  332. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/link-dev.js +0 -0
  333. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/list.js +0 -0
  334. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/remove.js +0 -0
  335. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/rm.js +0 -0
  336. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/set.js +0 -0
  337. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/unset.js +0 -0
  338. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/env/use.js +0 -0
  339. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/add.js +0 -0
  340. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/arguments.json +0 -0
  341. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/build.js +0 -0
  342. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/dot.js +0 -0
  343. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/get.js +0 -0
  344. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/help.js +0 -0
  345. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/help.txt +0 -0
  346. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/init.js +0 -0
  347. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/link-node-modules.js +0 -0
  348. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/link.js +0 -0
  349. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/list.js +0 -0
  350. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/man.js +0 -0
  351. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/msg.json +0 -0
  352. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/open.js +0 -0
  353. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/remove.js +0 -0
  354. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/reset.js +0 -0
  355. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/restart.js +0 -0
  356. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/set.js +0 -0
  357. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/start.js +0 -0
  358. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/status.js +0 -0
  359. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/stop.js +0 -0
  360. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/tail.js +0 -0
  361. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/update.js +0 -0
  362. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/framework/version.js +0 -0
  363. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/gina-dev.1.md +0 -0
  364. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/gina-framework.1.md +0 -0
  365. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/gina.1.md +0 -0
  366. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/helper.js +0 -0
  367. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/add.js +0 -0
  368. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/arguments.json +0 -0
  369. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/export.js +0 -0
  370. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/help.js +0 -0
  371. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/help.txt +0 -0
  372. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/import.js +0 -0
  373. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/i18n/scan.js +0 -0
  374. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/_host.js +0 -0
  375. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/arguments.json +0 -0
  376. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/build.js +0 -0
  377. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/help.js +0 -0
  378. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/help.txt +0 -0
  379. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/list.js +0 -0
  380. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/man.js +0 -0
  381. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/rm.js +0 -0
  382. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/image/run.js +0 -0
  383. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/index.js +0 -0
  384. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/inspector/help.js +0 -0
  385. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/inspector/help.txt +0 -0
  386. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/inspector/open.js +0 -0
  387. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/man-render.js +0 -0
  388. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/minion/arguments.json +0 -0
  389. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/minion/help.js +0 -0
  390. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/minion/help.txt +0 -0
  391. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/minion/kill.js +0 -0
  392. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/minion/list.js +0 -0
  393. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/msg.json +0 -0
  394. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/port/help.js +0 -0
  395. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/port/help.txt +0 -0
  396. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/port/inc/scan.js +0 -0
  397. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/port/list.js +0 -0
  398. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/port/reset.js +0 -0
  399. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/port/set.js +0 -0
  400. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/add.js +0 -0
  401. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/arguments.json +0 -0
  402. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/backup.js +0 -0
  403. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/build.js +0 -0
  404. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/help.js +0 -0
  405. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/help.txt +0 -0
  406. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/import.js +0 -0
  407. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/list.js +0 -0
  408. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/man.js +0 -0
  409. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/move.js +0 -0
  410. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/remove.js +0 -0
  411. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/rename.js +0 -0
  412. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/restart.js +0 -0
  413. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/restore.js +0 -0
  414. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/rm.js +0 -0
  415. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/start.js +0 -0
  416. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/status.js +0 -0
  417. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/project/stop.js +0 -0
  418. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/protocol/arguments.json +0 -0
  419. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/protocol/help.js +0 -0
  420. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/protocol/help.txt +0 -0
  421. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/protocol/list.js +0 -0
  422. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/protocol/remove.js +0 -0
  423. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/protocol/set.js +0 -0
  424. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/add.js +0 -0
  425. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/help.js +0 -0
  426. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/help.txt +0 -0
  427. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/link-local.js +0 -0
  428. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/link-production.js +0 -0
  429. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/list.js +0 -0
  430. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/remove.js +0 -0
  431. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/rm.js +0 -0
  432. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/scope/use.js +0 -0
  433. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/secrets/arguments.json +0 -0
  434. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/secrets/check.js +0 -0
  435. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/secrets/help.js +0 -0
  436. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/secrets/help.txt +0 -0
  437. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/secrets/scan.js +0 -0
  438. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/service/help.js +0 -0
  439. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/service/help.txt +0 -0
  440. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/service/list.js +0 -0
  441. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/service/man.js +0 -0
  442. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/service/start.js +0 -0
  443. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/storage/arguments.json +0 -0
  444. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/storage/gc.js +0 -0
  445. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/storage/help.txt +0 -0
  446. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/storage/stats.js +0 -0
  447. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/storage/verify.js +0 -0
  448. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd/view/add.js +0 -0
  449. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd-status-format/package.json +0 -0
  450. /package/framework/{v0.6.20 → v0.6.21}/lib/cmd-status-format/src/main.js +0 -0
  451. /package/framework/{v0.6.20 → v0.6.21}/lib/collection/README.md +0 -0
  452. /package/framework/{v0.6.20 → v0.6.21}/lib/collection/build.json +0 -0
  453. /package/framework/{v0.6.20 → v0.6.21}/lib/collection/package.json +0 -0
  454. /package/framework/{v0.6.20 → v0.6.21}/lib/collection/src/main.js +0 -0
  455. /package/framework/{v0.6.20 → v0.6.21}/lib/config.js +0 -0
  456. /package/framework/{v0.6.20 → v0.6.21}/lib/connector-config/package.json +0 -0
  457. /package/framework/{v0.6.20 → v0.6.21}/lib/connector-config/src/main.js +0 -0
  458. /package/framework/{v0.6.20 → v0.6.21}/lib/connector-error/package.json +0 -0
  459. /package/framework/{v0.6.20 → v0.6.21}/lib/connector-error/src/main.js +0 -0
  460. /package/framework/{v0.6.20 → v0.6.21}/lib/connector-registry/package.json +0 -0
  461. /package/framework/{v0.6.20 → v0.6.21}/lib/connector-registry/src/main.js +0 -0
  462. /package/framework/{v0.6.20 → v0.6.21}/lib/cron/README.md +0 -0
  463. /package/framework/{v0.6.20 → v0.6.21}/lib/cron/package.json +0 -0
  464. /package/framework/{v0.6.20 → v0.6.21}/lib/cron/src/main.js +0 -0
  465. /package/framework/{v0.6.20 → v0.6.21}/lib/domain/LICENSE +0 -0
  466. /package/framework/{v0.6.20 → v0.6.21}/lib/domain/README.md +0 -0
  467. /package/framework/{v0.6.20 → v0.6.21}/lib/domain/package.json +0 -0
  468. /package/framework/{v0.6.20 → v0.6.21}/lib/domain/src/main.js +0 -0
  469. /package/framework/{v0.6.20 → v0.6.21}/lib/dto/package.json +0 -0
  470. /package/framework/{v0.6.20 → v0.6.21}/lib/dto/src/main.js +0 -0
  471. /package/framework/{v0.6.20 → v0.6.21}/lib/dto-pipe/package.json +0 -0
  472. /package/framework/{v0.6.20 → v0.6.21}/lib/dto-pipe/src/main.js +0 -0
  473. /package/framework/{v0.6.20 → v0.6.21}/lib/dto-types/package.json +0 -0
  474. /package/framework/{v0.6.20 → v0.6.21}/lib/dto-types/src/main.js +0 -0
  475. /package/framework/{v0.6.20 → v0.6.21}/lib/generator/index.js +0 -0
  476. /package/framework/{v0.6.20 → v0.6.21}/lib/i18n/package.json +0 -0
  477. /package/framework/{v0.6.20 → v0.6.21}/lib/i18n/src/main.js +0 -0
  478. /package/framework/{v0.6.20 → v0.6.21}/lib/image-build/package.json +0 -0
  479. /package/framework/{v0.6.20 → v0.6.21}/lib/image-build/src/main.js +0 -0
  480. /package/framework/{v0.6.20 → v0.6.21}/lib/index.js +0 -0
  481. /package/framework/{v0.6.20 → v0.6.21}/lib/inherits/LICENSE +0 -0
  482. /package/framework/{v0.6.20 → v0.6.21}/lib/inherits/README.md +0 -0
  483. /package/framework/{v0.6.20 → v0.6.21}/lib/inherits/package.json +0 -0
  484. /package/framework/{v0.6.20 → v0.6.21}/lib/inherits/src/main.js +0 -0
  485. /package/framework/{v0.6.20 → v0.6.21}/lib/inspector-events/package.json +0 -0
  486. /package/framework/{v0.6.20 → v0.6.21}/lib/inspector-events/src/main.js +0 -0
  487. /package/framework/{v0.6.20 → v0.6.21}/lib/inspector-redact/package.json +0 -0
  488. /package/framework/{v0.6.20 → v0.6.21}/lib/inspector-redact/src/main.js +0 -0
  489. /package/framework/{v0.6.20 → v0.6.21}/lib/instrument/package.json +0 -0
  490. /package/framework/{v0.6.20 → v0.6.21}/lib/instrument/src/main.js +0 -0
  491. /package/framework/{v0.6.20 → v0.6.21}/lib/job/package.json +0 -0
  492. /package/framework/{v0.6.20 → v0.6.21}/lib/job/src/main.js +0 -0
  493. /package/framework/{v0.6.20 → v0.6.21}/lib/job-store.js +0 -0
  494. /package/framework/{v0.6.20 → v0.6.21}/lib/json-config-header/package.json +0 -0
  495. /package/framework/{v0.6.20 → v0.6.21}/lib/json-config-header/src/main.js +0 -0
  496. /package/framework/{v0.6.20 → v0.6.21}/lib/kv/package.json +0 -0
  497. /package/framework/{v0.6.20 → v0.6.21}/lib/kv/src/main.js +0 -0
  498. /package/framework/{v0.6.20 → v0.6.21}/lib/kv-store.js +0 -0
  499. /package/framework/{v0.6.20 → v0.6.21}/lib/loading-state/package.json +0 -0
  500. /package/framework/{v0.6.20 → v0.6.21}/lib/loading-state/src/main.js +0 -0
  501. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/README.md +0 -0
  502. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/package.json +0 -0
  503. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/default/index.js +0 -0
  504. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/file/index.js +0 -0
  505. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/file/lib/logrotator/README.md +0 -0
  506. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/file/lib/logrotator/index.js +0 -0
  507. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/mq/index.js +0 -0
  508. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/mq/listener.js +0 -0
  509. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/containers/mq/speaker.js +0 -0
  510. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/helper.js +0 -0
  511. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/main.js +0 -0
  512. /package/framework/{v0.6.20 → v0.6.21}/lib/logger/src/redact.js +0 -0
  513. /package/framework/{v0.6.20 → v0.6.21}/lib/maintenance/package.json +0 -0
  514. /package/framework/{v0.6.20 → v0.6.21}/lib/maintenance/src/main.js +0 -0
  515. /package/framework/{v0.6.20 → v0.6.21}/lib/math/index.js +0 -0
  516. /package/framework/{v0.6.20 → v0.6.21}/lib/mcp-dispatch/package.json +0 -0
  517. /package/framework/{v0.6.20 → v0.6.21}/lib/mcp-dispatch/src/main.js +0 -0
  518. /package/framework/{v0.6.20 → v0.6.21}/lib/mcp-http/package.json +0 -0
  519. /package/framework/{v0.6.20 → v0.6.21}/lib/mcp-http/src/main.js +0 -0
  520. /package/framework/{v0.6.20 → v0.6.21}/lib/mcp-server/package.json +0 -0
  521. /package/framework/{v0.6.20 → v0.6.21}/lib/mcp-server/src/main.js +0 -0
  522. /package/framework/{v0.6.20 → v0.6.21}/lib/merge/README.md +0 -0
  523. /package/framework/{v0.6.20 → v0.6.21}/lib/merge/package.json +0 -0
  524. /package/framework/{v0.6.20 → v0.6.21}/lib/metrics/package.json +0 -0
  525. /package/framework/{v0.6.20 → v0.6.21}/lib/metrics/src/main.js +0 -0
  526. /package/framework/{v0.6.20 → v0.6.21}/lib/model.js +0 -0
  527. /package/framework/{v0.6.20 → v0.6.21}/lib/net-locality/package.json +0 -0
  528. /package/framework/{v0.6.20 → v0.6.21}/lib/net-locality/src/main.js +0 -0
  529. /package/framework/{v0.6.20 → v0.6.21}/lib/nunjucks-filters/README.md +0 -0
  530. /package/framework/{v0.6.20 → v0.6.21}/lib/nunjucks-filters/package.json +0 -0
  531. /package/framework/{v0.6.20 → v0.6.21}/lib/nunjucks-filters/src/main.js +0 -0
  532. /package/framework/{v0.6.20 → v0.6.21}/lib/nunjucks-resolver/package.json +0 -0
  533. /package/framework/{v0.6.20 → v0.6.21}/lib/nunjucks-resolver/src/main.js +0 -0
  534. /package/framework/{v0.6.20 → v0.6.21}/lib/proc.js +0 -0
  535. /package/framework/{v0.6.20 → v0.6.21}/lib/push/package.json +0 -0
  536. /package/framework/{v0.6.20 → v0.6.21}/lib/push/src/main.js +0 -0
  537. /package/framework/{v0.6.20 → v0.6.21}/lib/rate-limit/package.json +0 -0
  538. /package/framework/{v0.6.20 → v0.6.21}/lib/rate-limit/src/main.js +0 -0
  539. /package/framework/{v0.6.20 → v0.6.21}/lib/release-watch/package.json +0 -0
  540. /package/framework/{v0.6.20 → v0.6.21}/lib/release-watch/src/main.js +0 -0
  541. /package/framework/{v0.6.20 → v0.6.21}/lib/render-cache/package.json +0 -0
  542. /package/framework/{v0.6.20 → v0.6.21}/lib/render-cache/src/main.js +0 -0
  543. /package/framework/{v0.6.20 → v0.6.21}/lib/render-cache-store.js +0 -0
  544. /package/framework/{v0.6.20 → v0.6.21}/lib/routing/README.md +0 -0
  545. /package/framework/{v0.6.20 → v0.6.21}/lib/routing/build.json +0 -0
  546. /package/framework/{v0.6.20 → v0.6.21}/lib/routing/package.json +0 -0
  547. /package/framework/{v0.6.20 → v0.6.21}/lib/routing/src/radix.js +0 -0
  548. /package/framework/{v0.6.20 → v0.6.21}/lib/routing-introspect/package.json +0 -0
  549. /package/framework/{v0.6.20 → v0.6.21}/lib/routing-introspect/src/main.js +0 -0
  550. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/package.json +0 -0
  551. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/backends/env.js +0 -0
  552. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/backends/exec.js +0 -0
  553. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/backends/file.js +0 -0
  554. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/declaration.js +0 -0
  555. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/env-file.js +0 -0
  556. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/main.js +0 -0
  557. /package/framework/{v0.6.20 → v0.6.21}/lib/secrets/src/sources.js +0 -0
  558. /package/framework/{v0.6.20 → v0.6.21}/lib/session-store.js +0 -0
  559. /package/framework/{v0.6.20 → v0.6.21}/lib/shell.js +0 -0
  560. /package/framework/{v0.6.20 → v0.6.21}/lib/sqlite-driver.js +0 -0
  561. /package/framework/{v0.6.20 → v0.6.21}/lib/state.js +0 -0
  562. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/package.json +0 -0
  563. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/local-cas.js +0 -0
  564. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/local-stream.js +0 -0
  565. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/local.js +0 -0
  566. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/main.js +0 -0
  567. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/meta-store.js +0 -0
  568. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/s3.js +0 -0
  569. /package/framework/{v0.6.20 → v0.6.21}/lib/storage/src/util.js +0 -0
  570. /package/framework/{v0.6.20 → v0.6.21}/lib/storage-store.js +0 -0
  571. /package/framework/{v0.6.20 → v0.6.21}/lib/swig-filters/README.md +0 -0
  572. /package/framework/{v0.6.20 → v0.6.21}/lib/swig-filters/package.json +0 -0
  573. /package/framework/{v0.6.20 → v0.6.21}/lib/swig-filters/src/main.js +0 -0
  574. /package/framework/{v0.6.20 → v0.6.21}/lib/swig-resolver/package.json +0 -0
  575. /package/framework/{v0.6.20 → v0.6.21}/lib/swig-resolver/src/main.js +0 -0
  576. /package/framework/{v0.6.20 → v0.6.21}/lib/template-loaders/package.json +0 -0
  577. /package/framework/{v0.6.20 → v0.6.21}/lib/template-loaders/src/loaders/http.js +0 -0
  578. /package/framework/{v0.6.20 → v0.6.21}/lib/template-loaders/src/loaders/memory.js +0 -0
  579. /package/framework/{v0.6.20 → v0.6.21}/lib/template-loaders/src/main.js +0 -0
  580. /package/framework/{v0.6.20 → v0.6.21}/lib/url/README.md +0 -0
  581. /package/framework/{v0.6.20 → v0.6.21}/lib/url/index.js +0 -0
  582. /package/framework/{v0.6.20 → v0.6.21}/lib/url/routing.json +0 -0
  583. /package/framework/{v0.6.20 → v0.6.21}/lib/uuid/package.json +0 -0
  584. /package/framework/{v0.6.20 → v0.6.21}/lib/uuid/src/main.js +0 -0
  585. /package/framework/{v0.6.20 → v0.6.21}/lib/validator.js +0 -0
  586. /package/framework/{v0.6.20 → v0.6.21}/lib/watcher/package.json +0 -0
  587. /package/framework/{v0.6.20 → v0.6.21}/lib/watcher/src/main.js +0 -0
  588. /package/framework/{v0.6.20 → v0.6.21}/lib/ws-framing/package.json +0 -0
  589. /package/framework/{v0.6.20 → v0.6.21}/lib/ws-framing/src/main.js +0 -0
  590. /package/framework/{v0.6.20 → v0.6.21}/lib/ws-query/package.json +0 -0
  591. /package/framework/{v0.6.20 → v0.6.21}/lib/ws-query/src/main.js +0 -0
  592. /package/framework/{v0.6.20 → v0.6.21}/lib/ws-session/package.json +0 -0
  593. /package/framework/{v0.6.20 → v0.6.21}/lib/ws-session/src/main.js +0 -0
package/llms.txt CHANGED
@@ -898,7 +898,7 @@ Dev-mode query instrumentation captures every database query tied to the current
898
898
 
899
899
  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).
900
900
 
901
- 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, 0.6.3, `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, 0.6.3, `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, 0.6.3, `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` from the in-flight lock (since #B312 the framework's only `aria-disabled` write), 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. #B319 (0.6.5-alpha.2): the focus-driven hide is EXEMPT during the framework's own ANSWER focus — both refused-submit paths (the #B246/#B308 display-only reveal via `focusFirstInvalidField`, and the `validate.<id>` failure branch's inline focus twin) render errors then focus the first invalid field, and that focus's synchronous `focusin` re-entered the live-check listener and hid the just-rendered message: a refused submit explained itself only to a screen reader (the #A11Y5 clip kept the node resolvable) while sighted users saw nothing — despite the render itself being VISIBLE (no fieldName ⇒ the live-check branch is skipped, and activeElement is still the trigger/BODY at render time), which is also why an async `query` rule in a repro is incidental (the suppressing dispatch is synchronous inside `focus()`). Fix: a one-shot module flag raised around BOTH focus loops (try/finally, cleared on every exit) gates the focusin arm's `refreshWarning` call; the message stays visible with the hard `form-item-error` styling, aria state untouched, and the first later keystroke re-engages the mid-typing suppression unchanged (in that answered-then-typed configuration the border stays `form-item-error` — the answer focus re-registered the field so `lastFocused` reads it twice and the isWarning heuristic keeps the hard border while the active-element ternary hides the message; the hidden message is the contract, the border there is heuristic). Behavioral lock: `test/e2e/validator-submit-answer-visibility.spec.js` (4 arms, red-first against the pre-fix bundle); browserless pins: `test/core/validator-answer-focus.test.js`. #B387 (shipped in 0.6.11): the one-shot flag covers only that synchronous window — on an async-`query` form with a committed error, a refused submit whose click lands inside the UNDRAINED completion tail of the previous settle had its answer delivered, focused, then re-hidden ~0.2ms later: a stale live-check waiter (woken inside the click's cascade by the reveal pass's deferred release) or the trailing silent global re-validation ran the display-refresh pair AFTER the flag's `finally` had cleared it, and BOTH hide sites key on the same heuristic — "the field is the active element, so the user is editing it" — which cannot tell answer-placed focus from user-placed focus (`refreshWarning`'s error→warning downgrade appends ` hidden`; `handleErrorsDisplay`'s refresh branch re-creates the message born-hidden via its active-element ternary; the occurrence gate is click-inside-the-tail, which is why full-suite/CI runs flaked ~1/20 while standalone runs stayed green). Fix: focus PROVENANCE — a single module slot `answerFocusHold` ({formId, elName}; one slot is exact, only one active element exists) recorded at both confirmed answer-focus points (inside the #B319 windows), consulted by BOTH hide sites for ANY caller however late (provenance beats a pass-staleness latch: the trailing re-validation is a FRESH pass spawned inside the click cascade and would sail through any staleness check), and released on the first genuine user interaction — any TRUSTED native event reaching one of the seven form proxy handlers while the one-shot flag is down (framework `triggerEvent` dispatches are untrusted and cannot release it; the answer's own trusted synchronous focusin is excluded by the flag) — so the deliberate mid-typing suppression re-engages the moment the user actually edits; no timers. Locked by `test/core/validator-answer-focus-hold.test.js` (17 — source pins on every edit site, comment-stripped extracted-real-bytes behavioral arms for both hide sites + the release helper, red-first against the pre-fix bytes; gina.js dist pins) and the §02 e2e arm (25/25 post-fix vs the ~1/20 pre-fix CI red whose signature was `Received: 1` + msgClass `hidden` — a recurrence of that signature is a NEW defect, not #B387). #B348 (shipped in 0.6.11): `revealValidationState`'s completion STARVED on any form whose async `query` field was not declared last — the reveal pass is un-latched, writes neither `isSubmitting` nor `isValidating`, and on a valid form its verdict is clean, so its waiter completion matched NO dispatch branch (terminal errors>0 / the latched dispatch / last-field / the display-only live-check arm): `onDisabledTriggerReveal` never ran, the stale `data-gina-form-submit-gated` marker never re-synced, and a fully valid form ate every later click until reload (measured live: 2 post-settle clicks, 0 POSTs — field declaration order decided whether the documented self-heal worked). Fix: the reveal's callback carries a completion identity (`onDisabledTriggerReveal.isRevealCompletion = true`, the engine's own `cb._data`/`cb._errors` property idiom) and the waiter chain gains ONE else-if chained after the display-only arm, gated on the SAME terminal condition the errors>0 block uses (`hasParsedAllRules && asyncCount <= 0` — the guard that stops a multi-query-field early wake, where the first waiter fires at asyncCount 1): a terminal reveal completion that matched no other branch dispatches `validated.<formId>` with its own cb. Every previously-working shape is byte-identical (errored reveals keep completing via the terminal branch, query-last via last-field, latched submits via the latched dispatch, live-check stays display-only), and the un-latched programmatic-submit starve (#B347) is deliberately untouched — its cb carries no marker and its fix is gated on its own repro. Known residual, pre-existing on EVERY branch: a query field whose own rule object continues past `query` never sets the terminal flag and still starves — same guard as the existing dispatch, no new asymmetry. Locked by `test/e2e/validator-reveal-starve.spec.js` (red-first: the starve arm failed on the pre-fix bundle while the query-LAST control arm passed on the same bytes, pinning the defect to field order; the scene manufactures [valid values + stale gate + stale committed error] deterministically via a prototype-setter silent fill after an errored reveal, with every precondition an explicit expect so an impossible scene voids loudly) and `test/core/validator-reveal-completion.test.js` (9 — stamp/consult/placement pins, extracted-real-bytes reveal arm asserting the stamp as a runtime value, gina.js verbatim pins AND a gina.min.js exact-count pin — unlike a local flag, the property name survives Closure). The not-ready submit trigger is marked `data-gina-form-submit-gated="true"` + the class `gina-form-submit-disabled` (#B312 retired `aria-disabled` from this marker — its contract says not-operable while the #B246 gate deliberately answers the click with the error reveal; authored `aria-disabled` remains enforced by the gates and is never auto-cleared), 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 **the framework now ships a default not-ready look (cursor `not-allowed` + dim; deliberately no `pointer-events`, which would swallow the click the reveal answers) that consumer CSS overrides**. 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 marked not-ready at bind under default-on live-check — `data-gina-form-submit-gated` + class since #B312 — 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, 0.6.4).** 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 not-ready submit trigger really refuses the send — and its marker is aria-free (#B246 + #B312, 0.6.5).** `updateSubmitTriggerState()` marks an invalid form's trigger with `data-gina-form-submit-gated="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) — and, since #B312, never `aria-disabled`: the gate ANSWERS the click with the error reveal, which that contract forbids for a control announced disabled, so the attribute belongs to consumers (authored marks the gates enforce and never auto-clear) and to the anchor in-flight lock. #B246's origin: NOTHING READ the marker — 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. `clickProxyHandler` now intercepts a disabled-or-gated 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 not-ready marker on a form that has since become valid heals itself — the heal touches only the marker + class; authored `aria-disabled` and the in-flight lock survive it (#B313 closed by construction). 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 on the shared channels (authored `aria-disabled` + native `disabled`), so both subsystems agree there; since #B312 it ALSO fires on the validator-owned not-ready marker `data-gina-form-submit-gated="true"`. **#B293 (0.6.5) NARROWED that predicate: the native `disabled` ATTRIBUTE now counts only where `disabled` is not a real IDL property (`!('disabled' in $el)`).** As first shipped it accepted native `disabled` on ANY element, which silently killed the near-universal double-submit guard: a click listener on the submit button that sets `disabled` to block a second submit is bound to the button and therefore runs BEFORE gina's delegated form-level proxy reaches the gate, so the gate saw the attribute, cancelled the click, and `send()` never ran — then the consumer's own handler cleared the attribute again, leaving NOTHING marked, a normal-looking button, and every subsequent click equally dead. Measured: a natively-disabled `<button>` delivers **no click to JS at all** (the browser suppresses it), so on a real form control that arm could only ever fire on an attribute set DURING the dispatch — it protected nothing and cost everything; on an `<a>` or a custom element the browser enforces nothing, so there the attribute is still the only honest signal and is still read (`'disabled' in $el` measured: button/input `true`, anchor/custom-element/span `false`). Regression cover is an **e2e** (`test/e2e/validator-submit-native-disabled.spec.js`, 4 arms), deliberately NOT a replica: the defect is an ordering interaction between a consumer's own listener and gina's delegated proxy, which only the real bundle handling a real click can exhibit — `test/core/validator-submit-trigger-state.test.js` covers the same gate with replicas and stayed GREEN throughout, including two tests named as controls for the working case. 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, 0.6.4).** 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. **#B295 (2026-08-06) — an async `query` rule left the form's validity state STALE once it settled.** `onasyncCompleted` derives its verdict from `d.getErrors(field)`, which `form-validator.js` scopes to a SINGLE field, so its `isFormValid` means "this field passed", never "the form is valid" — and the entire update block sits behind `if (!isFormValid && …)`. A form that had just become valid therefore received NO update: `updateSubmitTriggerState` was never called, so the bind-time not-ready mark (then `aria-disabled="true"`; the `data-gina-form-submit-gated` marker since #B312) survived on a valid form and `$forms[id].errors` kept listing the field. Cosmetic until #B246 made the click gate read that marker — then the first click after the query settled was silently eaten, and only the second went through (`revealValidationState`'s re-sync self-heals it). Fixed by handing the verdict to the fresh whole-form pass the same function ALREADY runs for the not-last-field case (`needsGlobalReValidation`), whose unconditional `updateSubmitTriggerState($currentForm, gResult.isValid())` settles the marker while its `handleErrorsDisplay` empty-errors branch clears the stale record; measured to add NO extra round-trip, because the query rule's already-registered-listener guard short-circuits the re-run. **The measurement that CHOSE the fix, and the reason the two cheaper shapes are wrong:** with a second required field left invalid, that fresh pass reports its error while BOTH the field-scoped `cb._errors` AND the in-pass `d.getErrors()` report none — so un-gating the existing call, or letting `handleErrorsDisplay`'s empty-errors branch auto-enable via its own `updateSubmitTriggerState(..., true)`, would each have ENABLED submit on an invalid form and re-opened #B246's hole. Both were rejected on that arm, not on reading. Covered by `test/e2e/validator-async-query-revalidation.spec.js` (4 arms, red-first: 01+02 RED on the pre-fix dist, 03 "another field invalid ⇒ still blocked" and 04 "no query rule ⇒ sends" GREEN throughout); arm 01 deliberately waits on the QUERY settling rather than polling the trigger's not-ready marker, because polling the guard is exactly what let the pre-existing FACE e2e stay green through #B293. Browser-bundled ⇒ consumer pickup is restart AND re-bake. **#B294 (2026-08-06) — a submit trigger whose DOM NODE is REPLACED after binding (the AJAX / popin re-render shape) was PERMANENTLY and SILENTLY dead.** Submit binding is **two-stage and only the first stage is delegated**: the native click proxy is registered on the FORM (`:8090`, "Form-level proxies: capture bubbled events from in-tree controls") and survives any re-render, but the listener that does the work is attached to the trigger NODE by `bindSubmitEl` (`:8231`). `gina.events` is a **name → id-STRING** registry (`utils/events.js:42`), so the `submit.<id>` key outlives the node: after a `cloneNode(true)`+`replaceChild` the `:8037` dispatch gate still passes, `cancelEvent` suppresses the native submit, and `triggerEvent` fires at a node with no listener — so the form does not even fall back to a normal submit. Nothing self-heals (`bindForm` latches `binded` at `:8580` behind `:585`, cleared only by `unbindForm`; there is no `MutationObserver` and no `isConnected` check in the bind/dispatch path). **Pre-existing, NOT a 0.6.4 regression** — measured identically on published 0.6.3. Fixed by marking the bound node with a **JS expando** (`__ginaSubmitBoundFor`) and re-binding the live node at the dispatch gate when the marker is absent — measured: `cloneNode(true)` copies `data-*` attributes but NOT expandos, which is exactly why the inherited `dataset.ginaFormSubmitTriggerFor` is useless as a fresh-node test and was a red herring in the original filing. **The rejected alternative is worth recording:** delegating the custom `submit.<id>` event to the form also fixes it (measured), but was measured to RETAIN the `gina.events` key past `unbind` (`registryKeyDeleted:false` vs stock `true`), which would force an edit inside `unbindForm`'s removal loop — six guard variants all keyed on `gina.events[name] == element.id`. Covered by `test/e2e/validator-submit-trigger-rebind.spec.js` (4 arms, red-first: 01 single replacement and 03 double replacement RED pre-fix; 02 untouched-sends and 04 replaced-but-invalid-still-refuses GREEN throughout, so re-binding provably does not bypass validation). **Diagnostic reflex for any "it stopped working after we re-rendered" report: the question is not whether the click listener survived — it did, it is on the form — but whether the node carrying the stage-2 listener is still the node in the document.** Browser-bundled ⇒ restart AND re-bake. **The submit proxy's gesture gate reads the LIVE trigger (#B308, 0.6.5).** The per-form `submit` proxy enforced its disabled gate on a DOMParser copy of the form's innerHTML, testing only native `.disabled` on the copy — blind to the gate's own marker (then `aria-disabled`, `data-gina-form-submit-gated` since #B312, written by `updateSubmitTriggerState`) while SIGHTED on a native `disabled` written mid-dispatch by a double-submit guard (the exact attribute #B293 ruled must not count on a real control — so wrapped-label + double-submit guard was a dead submit, the #B293 signature alive on the submit path). A gated trigger's trusted gesture therefore ran the full collect → validate cycle and SENT whenever the values validated. Now an `e.isTrusted` submit on a form whose REGISTERED trigger is `isTriggerDisabled()` is cancelled and answered with the same display-only `revealValidationState()` as the #B246 click path (errors rendered, first invalid field focused, stale marker re-synced); programmatic `$forms[id].submit()` arrives untrusted (triggerEvent CustomEvent) and deliberately keeps the fresh-validate path — a programmatic submit is not operating the control, the fresh pass is the honest gate there, and cancelling on a cached marker would break fill-then-submit flows. Two gesture shapes reach this gate (probe-measured, Chromium): a wrapped-label click (`<button type="submit"><span>` — the span has no `.type`, so the click proxy never fires and the gesture surfaces as a native submit event), and Enter in a form with NO native submit button (e.g. an `<a data-gina-form-submit="true">` trigger — implicit submission fires a DIRECT trusted submit; with a native button present, Enter instead synthesizes a click on the default button that the #B246 CLICK guard already refuses while the marker stands, so that shape never needed this gate). Covered by `test/e2e/validator-submit-proxy-disabled-gate.spec.js` (5 arms, red-first BOTH directions via the committed `B308_PREFIX_BUNDLE` route-swap hook: label-click and A-tag-Enter RED pre-fix on an EVENT discriminator — the pre-fix cycle fires `validate.<id>` from the no-`on('submit')` branch, while the post-fix reveal is display-only and fires nothing; POSTs cannot discriminate, since an invalid form never sends on either side). Scene lesson for e2e authors: stale-marker scenes are racy BY CONSTRUCTION — a page-level `.value =` write on a bound field is heard SYNCHRONOUSLY (the value property is instrumented and the silent live validation repairs the marker inside the write), and a delayed global silent pass can heal a protocol-level fill's stale marker mid-arm — so pin gate behaviour on a genuinely-invalid form and discriminate on event emission. Browser-bundled ⇒ restart AND re-bake. **The anchor in-flight lock owns `aria-disabled` alone; the settle releases remove it unconditionally (#B309 → #B312, 0.6.5).** On an `<a data-gina-form-submit="true">` trigger send()'s in-flight lock and the not-ready gate historically SHARED `aria-disabled` with opposite lifecycles — the settle releases (the `loadend` fail-safe + the readyState-4 twin) removed it unconditionally, so a mid-flight gate re-mark was silently erased at settle (#B309), and the reveal's valid-form heal erased the LOCK mid-flight the same way (#B313). #B312's single-writer split dissolves the class structurally: the gate writes only `data-gina-form-submit-gated` + the class, the lock is the sole framework `aria-disabled` writer on anchors (buttons lock via native `disabled`), both settle releases are back to the plain unconditional `removeAttribute` (the interim #B309 verdict-stamp replay retired unreleased, its 18-test file with it), and the reveal's show branch never touches aria — so authored `aria-disabled` survives every framework pass (enforced by the gates, never auto-cleared: consumers relying on the old auto-clear-on-valid must migrate) and #B313 is closed by construction. Scene fact that shaped the design: live-check is DELIBERATELY quiet during a real in-flight submit (the #B192 valid branch holds the submit latch until settle), so keystrokes cannot re-mark mid-flight — the reachable mid-flight writer was the #B246/#B308 reveal itself. Covered by `test/core/validator-submit-gated-marker.test.js` (17 — block-scoped single-writer negatives immune to right-extension, authored-aria + lock survival behaviour, the shipped `isTriggerDisabled` EXTRACTED and EXECUTED, terminator-anchored release-shape pins, dist-fidelity pins validated red-first across the rebuild). Browser-bundled ⇒ restart AND re-bake. **isRequired's emptiness is anchored at both ends, and trim strips both sides (#B245, 0.6.5).** The emptiness conjunct was `!/^\s+/.test(value)` — leading-anchor only — so ANY leading-whitespace-padded value read as empty (" x" rejected, "x " accepted), and under the documented isRequired-first ordering a paired `trim` healed `local.data` AFTER the error was recorded: a rejected field carried the trimmed non-empty value in the same result. Now `/^\s+$/`: whitespace-ONLY is empty, padded values pass, all-whitespace and the empty string still fail. Same fix: `trim`'s replace gained its missing `g` flag (first-match-only rewriting left the trailing run whenever a leading run matched — " x " → "x "). The validation-rules reference is corrected (it had documented the leading-space rejection as intended while prescribing the isRequired-then-trim pairing that could not work under it). Covered by `test/core/validator-required-whitespace-trim.test.js` (block-scoped comment-stripped source pins + the heal matrix on the REAL plugin over the server auto path + dist-fidelity counts red-first across the rebuild). Browser-bundled ⇒ restart AND re-bake. #B345 (issue #59, shipped in 0.6.7): the #SCS1e paren/`return` strip used to run BEFORE the regex-vs-comparison branch split, so a parenthesized `is` regex literal compiled from MANGLED text — groups destroyed and anchors rebound (`/^(a|b)$/` behaved as `/^a|b$/`: substring-permissive for middle alternatives), quantified groups requantified (`(#TAG)?` → literal `#TA` + optional `G`), and a literal `return` inside a pattern deleted — all silent, since the stripped text still compiled as a valid regex (the `Invalid regex literal` throw structurally cannot catch it). The strip now lives inside the binary-comparison branch only (grammar lock + authored-paren tolerance unchanged — `("a") === ("a")` still strips to a valid comparison); a regex literal compiles exactly as authored, that branch never evaluating the condition as JS. Behavior change (changelog ACTION REQUIRED): paren-carrying `is` patterns now match as authored, i.e. stricter; and the paren-wrapped-regex edge `(/foo/)` now fails closed in the comparison branch instead of being unwrapped into the regex branch. Locked by `test/lib/validator-is-regex-parens.test.js` (14 — red-first source ORDER pins, the reported reproduction, anchors/quantifier/`return` arms, comparison-branch controls, gina.js + wrap-immune gina.min.js order pins). #B389 (gh issue #63, shipped in 0.6.11): the Safari-autocomplete keydown interception (`handleAutoComplete` — REAL-Safari-only per #B135; preventDefault + programmatic value rebuild behind a transient readonly) restored the caret only two setTimeout(0) hops after each rebuild, while a `.value` assignment parks the selection at the END of the field (measured on WebKit AND Chromium) — so a fast second keystroke read a stale `selectionStart` and composed scrambled text ("AXB" where the user typed "ABX"; deterministic whenever two keydowns share one task, which live-check work between keystrokes makes routine). Fixed with a synchronous caret commit after every rebuild (`commitCaret` — measured to stick, and to survive the readonly toggle, on both engines) plus an element-recorded desired-caret tracker (`_ginaAcCaret`/`_ginaAcPending`) the handler trusts while a restore is in flight; the deferred restore (`queueCaretRestore` — the autofill-suppression readonly dance, mechanism-identical) re-asserts the LATEST committed position instead of its own stale capture, and arrows commit through the same tracker so a pending restore cannot undo them. By-catches in the same switch, all fixed to native behavior: #B390 Backspace at position 0 deleted the FIRST character (native no-op), #B391 Delete with a selection starting at 0 ate one char MORE than the selection, #B392 ArrowLeft at position 0 wrapped `setSelectionRange(-1)` to the unsigned maximum and teleported the caret to the END (now floored at 0). Browser-bundled ⇒ pickup is bundle restart AND re-bake. Tests: `test/core/validator-autocomplete-caret.test.js` (red-first: extraction controls, source pins, extracted-real-bytes behavioral arms on a caret-to-end element model, dist pins) + the webkit-only e2e `test/e2e/validator-autocomplete-caret.spec.js` (full-stack real-WebKit reproduction, red-first "AXB" → green "ABX"; chromium/firefox skip — #B135 gates them out; runs in the on-demand cross-engine job). **#B200 (shipped in 0.6.12): a NON-STRING field value no longer aborts the whole validation pass.** `isEmail` and `isJsonWebToken` opened with a TRUTHY-only coercion (`(this.value) ? this.value.toLowerCase() : this.value`), so a truthy non-string — `123` / `true` / `[]` from a JSON body, or a checkbox boolean on the client — reached `.toLowerCase()` and threw; `trim` was WIDER still, its type guard present but COMMENTED OUT and no truthy guard either, so EVERY non-string threw, a falsy `0` included. In all three the rule driver's catch RE-THROWS (`[ ginaFormValidator ] could not evaluate …`), so one bad field kills every remaining one: server-side the request goes unvalidated, client-side the boot-time binding loop dies and later forms silently lose validation AND CSRF injection. Fixed by type-guarding each site against a precedent already in the file — the #B87 `query` coercion for the two rules, isFloat's identical guarded `.replace()` for `trim`. **Measured, and the load-bearing check: the guard does NOT open a #B199-style silent bypass** — a non-string passes through untouched and the rule's own regex then rejects it, so `isEmail`/`isJsonWebToken` record a normal rule-keyed error (`isValid === false`), while `trim`, being a transform, leaves it untransformed (the #B245 both-ends strip on real strings is unchanged). **Reusable rule: type-guard BEFORE a string coercion, never truthy-guard** — a truthy check admits every non-string except the falsy ones (the authn.md §4 lesson, and the same split that sent falsy values to #B199 and truthy ones here). Deliberately NOT fixed in that pass: `isDate` (its throw is a purpose-built catch — an error-contract design call, #B397). `toFloat`/`format` — plus the same-family `set` — were later fixed by #B398 (context-safe rules; `test/core/validator-context-safe-rules.test.js`): `toFloat` reads the live DOM value only when one is reachable and otherwise uses the submitted `this.value` (server-side that IS the raw value, so the rule is fully functional in both contexts; the missing comma that leaked `isFloatingWithCommas` as an implicit global is fixed with it); `set` assigns the value everywhere and guards its DOM write (`isGFFCtx && target`); `format` always worked in BOTH contexts after `isDate` — the filed "dateFormat prototype extension absent server-side" mechanism was REFUTED by measurement (helpers/index installs `Date.prototype.format` unconditionally, server included) — and a non-Date value now throws a NAMED authoring error (`apply isDate(mask) before format(mask)`) instead of the opaque `val.format is not a function` (`toFloat`/`format` were first misread as this same class by a probe whose STRING CONTROL also threw, which is what exposed them). Browser-bundled ⇒ pickup is bundle restart AND re-bake. Tests: `test/core/validator-nonstring-value-guards.test.js` (comment-stripped source pins red-first vs the pre-fix blob, behavioural arms asserting the VERDICT not merely the absence of a throw, wrap-agnostic dist pins red-first vs the pre-fix artifact, plus untouched-rule controls incl. one labelled INVARIANT because it cannot go red).
901
+ 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, 0.6.3, `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, 0.6.3, `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, 0.6.3, `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` from the in-flight lock (since #B312 the framework's only `aria-disabled` write), 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. #B319 (0.6.5-alpha.2): the focus-driven hide is EXEMPT during the framework's own ANSWER focus — both refused-submit paths (the #B246/#B308 display-only reveal via `focusFirstInvalidField`, and the `validate.<id>` failure branch's inline focus twin) render errors then focus the first invalid field, and that focus's synchronous `focusin` re-entered the live-check listener and hid the just-rendered message: a refused submit explained itself only to a screen reader (the #A11Y5 clip kept the node resolvable) while sighted users saw nothing — despite the render itself being VISIBLE (no fieldName ⇒ the live-check branch is skipped, and activeElement is still the trigger/BODY at render time), which is also why an async `query` rule in a repro is incidental (the suppressing dispatch is synchronous inside `focus()`). Fix: a one-shot module flag raised around BOTH focus loops (try/finally, cleared on every exit) gates the focusin arm's `refreshWarning` call; the message stays visible with the hard `form-item-error` styling, aria state untouched, and the first later keystroke re-engages the mid-typing suppression unchanged (in that answered-then-typed configuration the border stays `form-item-error` — the answer focus re-registered the field so `lastFocused` reads it twice and the isWarning heuristic keeps the hard border while the active-element ternary hides the message; the hidden message is the contract, the border there is heuristic). Behavioral lock: `test/e2e/validator-submit-answer-visibility.spec.js` (4 arms, red-first against the pre-fix bundle); browserless pins: `test/core/validator-answer-focus.test.js`. #B387 (shipped in 0.6.11): the one-shot flag covers only that synchronous window — on an async-`query` form with a committed error, a refused submit whose click lands inside the UNDRAINED completion tail of the previous settle had its answer delivered, focused, then re-hidden ~0.2ms later: a stale live-check waiter (woken inside the click's cascade by the reveal pass's deferred release) or the trailing silent global re-validation ran the display-refresh pair AFTER the flag's `finally` had cleared it, and BOTH hide sites key on the same heuristic — "the field is the active element, so the user is editing it" — which cannot tell answer-placed focus from user-placed focus (`refreshWarning`'s error→warning downgrade appends ` hidden`; `handleErrorsDisplay`'s refresh branch re-creates the message born-hidden via its active-element ternary; the occurrence gate is click-inside-the-tail, which is why full-suite/CI runs flaked ~1/20 while standalone runs stayed green). Fix: focus PROVENANCE — a single module slot `answerFocusHold` ({formId, elName}; one slot is exact, only one active element exists) recorded at both confirmed answer-focus points (inside the #B319 windows), consulted by BOTH hide sites for ANY caller however late (provenance beats a pass-staleness latch: the trailing re-validation is a FRESH pass spawned inside the click cascade and would sail through any staleness check), and released on the first genuine user interaction — any TRUSTED native event reaching one of the seven form proxy handlers while the one-shot flag is down (framework `triggerEvent` dispatches are untrusted and cannot release it; the answer's own trusted synchronous focusin is excluded by the flag) — so the deliberate mid-typing suppression re-engages the moment the user actually edits; no timers. Locked by `test/core/validator-answer-focus-hold.test.js` (17 — source pins on every edit site, comment-stripped extracted-real-bytes behavioral arms for both hide sites + the release helper, red-first against the pre-fix bytes; gina.js dist pins) and the §02 e2e arm (25/25 post-fix vs the ~1/20 pre-fix CI red whose signature was `Received: 1` + msgClass `hidden` — a recurrence of that signature is a NEW defect, not #B387). #B348 (shipped in 0.6.11): `revealValidationState`'s completion STARVED on any form whose async `query` field was not declared last — the reveal pass is un-latched, writes neither `isSubmitting` nor `isValidating`, and on a valid form its verdict is clean, so its waiter completion matched NO dispatch branch (terminal errors>0 / the latched dispatch / last-field / the display-only live-check arm): `onDisabledTriggerReveal` never ran, the stale `data-gina-form-submit-gated` marker never re-synced, and a fully valid form ate every later click until reload (measured live: 2 post-settle clicks, 0 POSTs — field declaration order decided whether the documented self-heal worked). Fix: the reveal's callback carries a completion identity (`onDisabledTriggerReveal.isRevealCompletion = true`, the engine's own `cb._data`/`cb._errors` property idiom) and the waiter chain gains ONE else-if chained after the display-only arm, gated on the SAME terminal condition the errors>0 block uses (`hasParsedAllRules && asyncCount <= 0` — the guard that stops a multi-query-field early wake, where the first waiter fires at asyncCount 1): a terminal reveal completion that matched no other branch dispatches `validated.<formId>` with its own cb. Every previously-working shape is byte-identical (errored reveals keep completing via the terminal branch, query-last via last-field, latched submits via the latched dispatch, live-check stays display-only), and the un-latched programmatic-submit starve (#B347) is deliberately untouched — its cb carries no marker and its fix is gated on its own repro. Known residual, pre-existing on EVERY branch: a query field whose own rule object continues past `query` never sets the terminal flag and still starves — same guard as the existing dispatch, no new asymmetry. Locked by `test/e2e/validator-reveal-starve.spec.js` (red-first: the starve arm failed on the pre-fix bundle while the query-LAST control arm passed on the same bytes, pinning the defect to field order; the scene manufactures [valid values + stale gate + stale committed error] deterministically via a prototype-setter silent fill after an errored reveal, with every precondition an explicit expect so an impossible scene voids loudly) and `test/core/validator-reveal-completion.test.js` (9 — stamp/consult/placement pins, extracted-real-bytes reveal arm asserting the stamp as a runtime value, gina.js verbatim pins AND a gina.min.js exact-count pin — unlike a local flag, the property name survives Closure). The not-ready submit trigger is marked `data-gina-form-submit-gated="true"` + the class `gina-form-submit-disabled` (#B312 retired `aria-disabled` from this marker — its contract says not-operable while the #B246 gate deliberately answers the click with the error reveal; authored `aria-disabled` remains enforced by the gates and is never auto-cleared), 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 **the framework now ships a default not-ready look (cursor `not-allowed` + dim; deliberately no `pointer-events`, which would swallow the click the reveal answers) that consumer CSS overrides**. 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 marked not-ready at bind under default-on live-check — `data-gina-form-submit-gated` + class since #B312 — 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, 0.6.4).** 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 not-ready submit trigger really refuses the send — and its marker is aria-free (#B246 + #B312, 0.6.5).** `updateSubmitTriggerState()` marks an invalid form's trigger with `data-gina-form-submit-gated="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) — and, since #B312, never `aria-disabled`: the gate ANSWERS the click with the error reveal, which that contract forbids for a control announced disabled, so the attribute belongs to consumers (authored marks the gates enforce and never auto-clear) and to the anchor in-flight lock. #B246's origin: NOTHING READ the marker — 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. `clickProxyHandler` now intercepts a disabled-or-gated 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 not-ready marker on a form that has since become valid heals itself — the heal touches only the marker + class; authored `aria-disabled` and the in-flight lock survive it (#B313 closed by construction). 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 on the shared channels (authored `aria-disabled` + native `disabled`), so both subsystems agree there; since #B312 it ALSO fires on the validator-owned not-ready marker `data-gina-form-submit-gated="true"`. **#B293 (0.6.5) NARROWED that predicate: the native `disabled` ATTRIBUTE now counts only where `disabled` is not a real IDL property (`!('disabled' in $el)`).** As first shipped it accepted native `disabled` on ANY element, which silently killed the near-universal double-submit guard: a click listener on the submit button that sets `disabled` to block a second submit is bound to the button and therefore runs BEFORE gina's delegated form-level proxy reaches the gate, so the gate saw the attribute, cancelled the click, and `send()` never ran — then the consumer's own handler cleared the attribute again, leaving NOTHING marked, a normal-looking button, and every subsequent click equally dead. Measured: a natively-disabled `<button>` delivers **no click to JS at all** (the browser suppresses it), so on a real form control that arm could only ever fire on an attribute set DURING the dispatch — it protected nothing and cost everything; on an `<a>` or a custom element the browser enforces nothing, so there the attribute is still the only honest signal and is still read (`'disabled' in $el` measured: button/input `true`, anchor/custom-element/span `false`). Regression cover is an **e2e** (`test/e2e/validator-submit-native-disabled.spec.js`, 4 arms), deliberately NOT a replica: the defect is an ordering interaction between a consumer's own listener and gina's delegated proxy, which only the real bundle handling a real click can exhibit — `test/core/validator-submit-trigger-state.test.js` covers the same gate with replicas and stayed GREEN throughout, including two tests named as controls for the working case. 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, 0.6.4).** 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. **#B295 (2026-08-06) — an async `query` rule left the form's validity state STALE once it settled.** `onasyncCompleted` derives its verdict from `d.getErrors(field)`, which `form-validator.js` scopes to a SINGLE field, so its `isFormValid` means "this field passed", never "the form is valid" — and the entire update block sits behind `if (!isFormValid && …)`. A form that had just become valid therefore received NO update: `updateSubmitTriggerState` was never called, so the bind-time not-ready mark (then `aria-disabled="true"`; the `data-gina-form-submit-gated` marker since #B312) survived on a valid form and `$forms[id].errors` kept listing the field. Cosmetic until #B246 made the click gate read that marker — then the first click after the query settled was silently eaten, and only the second went through (`revealValidationState`'s re-sync self-heals it). Fixed by handing the verdict to the fresh whole-form pass the same function ALREADY runs for the not-last-field case (`needsGlobalReValidation`), whose unconditional `updateSubmitTriggerState($currentForm, gResult.isValid())` settles the marker while its `handleErrorsDisplay` empty-errors branch clears the stale record; measured to add NO extra round-trip, because the query rule's already-registered-listener guard short-circuits the re-run. **The measurement that CHOSE the fix, and the reason the two cheaper shapes are wrong:** with a second required field left invalid, that fresh pass reports its error while BOTH the field-scoped `cb._errors` AND the in-pass `d.getErrors()` report none — so un-gating the existing call, or letting `handleErrorsDisplay`'s empty-errors branch auto-enable via its own `updateSubmitTriggerState(..., true)`, would each have ENABLED submit on an invalid form and re-opened #B246's hole. Both were rejected on that arm, not on reading. Covered by `test/e2e/validator-async-query-revalidation.spec.js` (4 arms, red-first: 01+02 RED on the pre-fix dist, 03 "another field invalid ⇒ still blocked" and 04 "no query rule ⇒ sends" GREEN throughout); arm 01 deliberately waits on the QUERY settling rather than polling the trigger's not-ready marker, because polling the guard is exactly what let the pre-existing FACE e2e stay green through #B293. Browser-bundled ⇒ consumer pickup is restart AND re-bake. **#B294 (2026-08-06) — a submit trigger whose DOM NODE is REPLACED after binding (the AJAX / popin re-render shape) was PERMANENTLY and SILENTLY dead.** Submit binding is **two-stage and only the first stage is delegated**: the native click proxy is registered on the FORM (`:8090`, "Form-level proxies: capture bubbled events from in-tree controls") and survives any re-render, but the listener that does the work is attached to the trigger NODE by `bindSubmitEl` (`:8231`). `gina.events` is a **name → id-STRING** registry (`utils/events.js:42`), so the `submit.<id>` key outlives the node: after a `cloneNode(true)`+`replaceChild` the `:8037` dispatch gate still passes, `cancelEvent` suppresses the native submit, and `triggerEvent` fires at a node with no listener — so the form does not even fall back to a normal submit. Nothing self-heals (`bindForm` latches `binded` at `:8580` behind `:585`, cleared only by `unbindForm`; there is no `MutationObserver` and no `isConnected` check in the bind/dispatch path). **Pre-existing, NOT a 0.6.4 regression** — measured identically on published 0.6.3. Fixed by marking the bound node with a **JS expando** (`__ginaSubmitBoundFor`) and re-binding the live node at the dispatch gate when the marker is absent — measured: `cloneNode(true)` copies `data-*` attributes but NOT expandos, which is exactly why the inherited `dataset.ginaFormSubmitTriggerFor` is useless as a fresh-node test and was a red herring in the original filing. **The rejected alternative is worth recording:** delegating the custom `submit.<id>` event to the form also fixes it (measured), but was measured to RETAIN the `gina.events` key past `unbind` (`registryKeyDeleted:false` vs stock `true`), which would force an edit inside `unbindForm`'s removal loop — six guard variants all keyed on `gina.events[name] == element.id`. Covered by `test/e2e/validator-submit-trigger-rebind.spec.js` (4 arms, red-first: 01 single replacement and 03 double replacement RED pre-fix; 02 untouched-sends and 04 replaced-but-invalid-still-refuses GREEN throughout, so re-binding provably does not bypass validation). **Diagnostic reflex for any "it stopped working after we re-rendered" report: the question is not whether the click listener survived — it did, it is on the form — but whether the node carrying the stage-2 listener is still the node in the document.** Browser-bundled ⇒ restart AND re-bake. **The submit proxy's gesture gate reads the LIVE trigger (#B308, 0.6.5).** The per-form `submit` proxy enforced its disabled gate on a DOMParser copy of the form's innerHTML, testing only native `.disabled` on the copy — blind to the gate's own marker (then `aria-disabled`, `data-gina-form-submit-gated` since #B312, written by `updateSubmitTriggerState`) while SIGHTED on a native `disabled` written mid-dispatch by a double-submit guard (the exact attribute #B293 ruled must not count on a real control — so wrapped-label + double-submit guard was a dead submit, the #B293 signature alive on the submit path). A gated trigger's trusted gesture therefore ran the full collect → validate cycle and SENT whenever the values validated. Now an `e.isTrusted` submit on a form whose REGISTERED trigger is `isTriggerDisabled()` is cancelled and answered with the same display-only `revealValidationState()` as the #B246 click path (errors rendered, first invalid field focused, stale marker re-synced); programmatic `$forms[id].submit()` arrives untrusted (triggerEvent CustomEvent) and deliberately keeps the fresh-validate path — a programmatic submit is not operating the control, the fresh pass is the honest gate there, and cancelling on a cached marker would break fill-then-submit flows. Two gesture shapes reach this gate (probe-measured, Chromium): a wrapped-label click (`<button type="submit"><span>` — the span has no `.type`, so the click proxy never fires and the gesture surfaces as a native submit event), and Enter in a form with NO native submit button (e.g. an `<a data-gina-form-submit="true">` trigger — implicit submission fires a DIRECT trusted submit; with a native button present, Enter instead synthesizes a click on the default button that the #B246 CLICK guard already refuses while the marker stands, so that shape never needed this gate). Covered by `test/e2e/validator-submit-proxy-disabled-gate.spec.js` (5 arms, red-first BOTH directions via the committed `B308_PREFIX_BUNDLE` route-swap hook: label-click and A-tag-Enter RED pre-fix on an EVENT discriminator — the pre-fix cycle fires `validate.<id>` from the no-`on('submit')` branch, while the post-fix reveal is display-only and fires nothing; POSTs cannot discriminate, since an invalid form never sends on either side). Scene lesson for e2e authors: stale-marker scenes are racy BY CONSTRUCTION — a page-level `.value =` write on a bound field is heard SYNCHRONOUSLY (the value property is instrumented and the silent live validation repairs the marker inside the write), and a delayed global silent pass can heal a protocol-level fill's stale marker mid-arm — so pin gate behaviour on a genuinely-invalid form and discriminate on event emission. Browser-bundled ⇒ restart AND re-bake. **The anchor in-flight lock owns `aria-disabled` alone; the settle releases remove it unconditionally (#B309 → #B312, 0.6.5).** On an `<a data-gina-form-submit="true">` trigger send()'s in-flight lock and the not-ready gate historically SHARED `aria-disabled` with opposite lifecycles — the settle releases (the `loadend` fail-safe + the readyState-4 twin) removed it unconditionally, so a mid-flight gate re-mark was silently erased at settle (#B309), and the reveal's valid-form heal erased the LOCK mid-flight the same way (#B313). #B312's single-writer split dissolves the class structurally: the gate writes only `data-gina-form-submit-gated` + the class, the lock is the sole framework `aria-disabled` writer on anchors (buttons lock via native `disabled`), both settle releases are back to the plain unconditional `removeAttribute` (the interim #B309 verdict-stamp replay retired unreleased, its 18-test file with it), and the reveal's show branch never touches aria — so authored `aria-disabled` survives every framework pass (enforced by the gates, never auto-cleared: consumers relying on the old auto-clear-on-valid must migrate) and #B313 is closed by construction. Scene fact that shaped the design: live-check is DELIBERATELY quiet during a real in-flight submit (the #B192 valid branch holds the submit latch until settle), so keystrokes cannot re-mark mid-flight — the reachable mid-flight writer was the #B246/#B308 reveal itself. Covered by `test/core/validator-submit-gated-marker.test.js` (17 — block-scoped single-writer negatives immune to right-extension, authored-aria + lock survival behaviour, the shipped `isTriggerDisabled` EXTRACTED and EXECUTED, terminator-anchored release-shape pins, dist-fidelity pins validated red-first across the rebuild). Browser-bundled ⇒ restart AND re-bake. **isRequired's emptiness is anchored at both ends, and trim strips both sides (#B245, 0.6.5).** The emptiness conjunct was `!/^\s+/.test(value)` — leading-anchor only — so ANY leading-whitespace-padded value read as empty (" x" rejected, "x " accepted), and under the documented isRequired-first ordering a paired `trim` healed `local.data` AFTER the error was recorded: a rejected field carried the trimmed non-empty value in the same result. Now `/^\s+$/`: whitespace-ONLY is empty, padded values pass, all-whitespace and the empty string still fail. Same fix: `trim`'s replace gained its missing `g` flag (first-match-only rewriting left the trailing run whenever a leading run matched — " x " → "x "). The validation-rules reference is corrected (it had documented the leading-space rejection as intended while prescribing the isRequired-then-trim pairing that could not work under it). Covered by `test/core/validator-required-whitespace-trim.test.js` (block-scoped comment-stripped source pins + the heal matrix on the REAL plugin over the server auto path + dist-fidelity counts red-first across the rebuild). Browser-bundled ⇒ restart AND re-bake. #B345 (issue #59, shipped in 0.6.7): the #SCS1e paren/`return` strip used to run BEFORE the regex-vs-comparison branch split, so a parenthesized `is` regex literal compiled from MANGLED text — groups destroyed and anchors rebound (`/^(a|b)$/` behaved as `/^a|b$/`: substring-permissive for middle alternatives), quantified groups requantified (`(#TAG)?` → literal `#TA` + optional `G`), and a literal `return` inside a pattern deleted — all silent, since the stripped text still compiled as a valid regex (the `Invalid regex literal` throw structurally cannot catch it). The strip now lives inside the binary-comparison branch only (grammar lock + authored-paren tolerance unchanged — `("a") === ("a")` still strips to a valid comparison); a regex literal compiles exactly as authored, that branch never evaluating the condition as JS. Behavior change (changelog ACTION REQUIRED): paren-carrying `is` patterns now match as authored, i.e. stricter; and the paren-wrapped-regex edge `(/foo/)` now fails closed in the comparison branch instead of being unwrapped into the regex branch. Locked by `test/lib/validator-is-regex-parens.test.js` (14 — red-first source ORDER pins, the reported reproduction, anchors/quantifier/`return` arms, comparison-branch controls, gina.js + wrap-immune gina.min.js order pins). #B389 (gh issue #63, shipped in 0.6.11): the Safari-autocomplete keydown interception (`handleAutoComplete` — REAL-Safari-only per #B135; preventDefault + programmatic value rebuild behind a transient readonly) restored the caret only two setTimeout(0) hops after each rebuild, while a `.value` assignment parks the selection at the END of the field (measured on WebKit AND Chromium) — so a fast second keystroke read a stale `selectionStart` and composed scrambled text ("AXB" where the user typed "ABX"; deterministic whenever two keydowns share one task, which live-check work between keystrokes makes routine). Fixed with a synchronous caret commit after every rebuild (`commitCaret` — measured to stick, and to survive the readonly toggle, on both engines) plus an element-recorded desired-caret tracker (`_ginaAcCaret`/`_ginaAcPending`) the handler trusts while a restore is in flight; the deferred restore (`queueCaretRestore` — the autofill-suppression readonly dance, mechanism-identical) re-asserts the LATEST committed position instead of its own stale capture, and arrows commit through the same tracker so a pending restore cannot undo them. By-catches in the same switch, all fixed to native behavior: #B390 Backspace at position 0 deleted the FIRST character (native no-op), #B391 Delete with a selection starting at 0 ate one char MORE than the selection, #B392 ArrowLeft at position 0 wrapped `setSelectionRange(-1)` to the unsigned maximum and teleported the caret to the END (now floored at 0). Browser-bundled ⇒ pickup is bundle restart AND re-bake. Tests: `test/core/validator-autocomplete-caret.test.js` (red-first: extraction controls, source pins, extracted-real-bytes behavioral arms on a caret-to-end element model, dist pins) + the webkit-only e2e `test/e2e/validator-autocomplete-caret.spec.js` (full-stack real-WebKit reproduction, red-first "AXB" → green "ABX"; chromium/firefox skip — #B135 gates them out; runs in the on-demand cross-engine job). **#B200 (shipped in 0.6.12): a NON-STRING field value no longer aborts the whole validation pass.** `isEmail` and `isJsonWebToken` opened with a TRUTHY-only coercion (`(this.value) ? this.value.toLowerCase() : this.value`), so a truthy non-string — `123` / `true` / `[]` from a JSON body, or a checkbox boolean on the client — reached `.toLowerCase()` and threw; `trim` was WIDER still, its type guard present but COMMENTED OUT and no truthy guard either, so EVERY non-string threw, a falsy `0` included. In all three the rule driver's catch RE-THROWS (`[ ginaFormValidator ] could not evaluate …`), so one bad field kills every remaining one: server-side the request goes unvalidated, client-side the boot-time binding loop dies and later forms silently lose validation AND CSRF injection. Fixed by type-guarding each site against a precedent already in the file — the #B87 `query` coercion for the two rules, isFloat's identical guarded `.replace()` for `trim`. **Measured, and the load-bearing check: the guard does NOT open a #B199-style silent bypass** — a non-string passes through untouched and the rule's own regex then rejects it, so `isEmail`/`isJsonWebToken` record a normal rule-keyed error (`isValid === false`), while `trim`, being a transform, leaves it untransformed (the #B245 both-ends strip on real strings is unchanged). **Reusable rule: type-guard BEFORE a string coercion, never truthy-guard** — a truthy check admits every non-string except the falsy ones (the authn.md §4 lesson, and the same split that sent falsy values to #B199 and truthy ones here). Deliberately NOT fixed in that pass: `isDate` (its throw is a purpose-built catch — an error-contract design call, #B397). `toFloat`/`format` — plus the same-family `set` — were later fixed by #B398 (context-safe rules; `test/core/validator-context-safe-rules.test.js`): `toFloat` reads the live DOM value only when one is reachable and otherwise uses the submitted `this.value` (server-side that IS the raw value, so the rule is fully functional in both contexts; the missing comma that leaked `isFloatingWithCommas` as an implicit global is fixed with it); `set` assigns the value everywhere and guards its DOM write (`isGFFCtx && target`); `format` always worked in BOTH contexts after `isDate` — the filed "dateFormat prototype extension absent server-side" mechanism was REFUTED by measurement (helpers/index installs `Date.prototype.format` unconditionally, server included) — and a non-Date value now throws a NAMED authoring error (`apply isDate(mask) before format(mask)`) instead of the opaque `val.format is not a function` (`toFloat`/`format` were first misread as this same class by a probe whose STRING CONTROL also threw, which is what exposed them). Browser-bundled ⇒ pickup is bundle restart AND re-bake. Tests: `test/core/validator-nonstring-value-guards.test.js` (comment-stripped source pins red-first vs the pre-fix blob, behavioural arms asserting the VERDICT not merely the absence of a throw, wrap-agnostic dist pins red-first vs the pre-fix artifact, plus untouched-rule controls incl. one labelled INVARIANT because it cannot go red). **The form-level keydown proxy defers the native-cancel decision to the namespaced handler (#B444, gh issue #67):** `addListener(gina, $el, 'keydown.<id>', fn)` registers a CUSTOM event type a native keydown can never fire; the form-level `keydownProxyHandler` bridges the two by re-dispatching - and it used to `cancelEvent()` the NATIVE keydown unconditionally BEFORE that dispatch, so on real-Safari UAs (where the autocomplete interception registers exactly such a handler) EVERY modifier chord on a live-checked autocomplete-suppressed field was dead - paste, select-all, copy, cut, undo - with NO paste/beforeinput event observable anywhere (preventDefault on a native keydown suppresses the browser editing command itself), while the interception handler's own chord bail sat one layer too low to help. The proxy now dispatches FIRST and cancels the native keydown only when the handler prevented the synthetic event (which the interception already does on the paths it re-implements and already does not on chords); `triggerEvent` returns the dispatched event to make that decision readable, undefined on the element-less path - treated fail-open to native. The keyup proxy is untouched (no `keyup.<id>` registrar exists). Measured live on WebKit: chords restored, typing interception + caret integrity + autofill suppression unchanged. Tests: `test/core/validator-keydown-proxy.test.js` (extract-and-execute on both changed functions) + `test/e2e/validator-autocomplete-paste.spec.js` (webkit project, cross-engine job).
902
902
 
903
903
  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). **The staged on-disk FILENAME is server-generated and opaque, never the client's (#B419, 0.6.16):** `crypto.randomBytes(16).toString('hex') + '.part'`, minted once per part and used at BOTH path-construction sites — the write stream AND `req.files[].path`, which are built independently and must never diverge. Pre-fix the destination was `<fileUploadDir>/<client basename>` with NO per-part component, so two parts sharing a name — within ONE request or across concurrent requests — opened two write streams on ONE path with independent file offsets and INTERLEAVED their bytes into a single hybrid matching neither source, while the framework answered `storeErr:false` + 200 (measured live pre-fix: 12 concurrent same-named uploads → 0/12 clean, one hybrid file; one request with two same-named parts → one 500000-byte file of {B:417353,A:82647}; post-fix 12/12 clean, each byte-exact). The parse precedes routing and all middleware, so an UNAUTHENTICATED POST to ANY url reaches it — an unrouted one answers 404 to the client and still writes the file. A CSPRNG rather than the `Math.random()` used by `movefiles` (V8's is xorshift128+, so staging paths would be predictable, and a group `path` is operator-configurable into a served root); a RANDOM name rather than `filename + <suffix>` (keeps attacker-controlled bytes off the filesystem — RTL-override display spoofing, control chars, reserved device names — and cannot overflow NAME_MAX=255 into an ENAMETOOLONG 500 for a long-but-legal client filename); no interpretable extension, so a misconfigured static-serve of the staging dir cannot hand back an executable type. This is the multer ("a random name that doesn't include any file extension") and formidable (`newFilename` hexoid) convention. `req.files[].originalFilename` still carries the client's name and is what `store()` publishes the file under (`controller.js:4061`) and what a storage driver receives as `originalName`, so the documented "keeping each file's original name" contract is UNCHANGED — only `req.files[].path`'s BASENAME differs, and that field is documented as the temporary file's path. Server-side ⇒ pickup is a bundle restart, no re-bake. **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. Since #STO1 slice 1 a group may also carry driver: '<name>', routing its self.store() step through settings.storage (entry #300; the parse/staging path above is unchanged). Server-side only. Tests: `test/core/upload-groups.test.js` + `upload-config.test.js` + `multipart-nonfile-terminal.test.js` + `multipart-field-capture.test.js` + `upload-concurrent-staging.test.js` (+ the client `send(FormData)` half: `validator-send-formdata-multipart-fields.test.js`). **Server-path integrity + terminal-state hardening (former #263/#267/#268/#270 — #B103/#B142/#B143/#B144/#B145/#B223, 0.5.22-0.5.24):** a multipart body stays RAW end-to-end — `request.isMultipart` is computed ONCE at the request prologue and gates the request-stream `setEncoding` (a multipart stream reaches busboy as raw Buffers, its documented input; the decode remains for the text-body branches), write-pipeline chunks pass through VERBATIM, and `req.files[].size` counts BYTES, never a decoded string's length (#B103 — pure-ASCII surviving both old decode layers is why text uploads always worked while every real binary corrupted on every native multipart client: a PNG's 0x89 magic landed as 0xFD). The per-file `size` is finalized in the liner Transform's `_flush` (#B142 — the source-'end' record ran one pipe hop UPSTREAM of the byte counter with 16-object high-water marks still queued, under-counting ~25% on a 1.5MB file while md5 stayed perfect: the bytes were intact, only the number lied; `_flush` runs after the last `_transform` and strictly before the write stream can emit 'finish', both interleavings measured exact), and BOTH write-stream terminal listeners arm AT STREAM CREATION in the 'file' handler (#B143 — arming inside `busboy.on('finish')` lost the race for any early-finishing small file: Node never replays 'finish' for a late listener, so a throttled two-file upload hung deterministically, ~1 in 13 unthrottled; unauthenticated, both engines). Resume fires on `busboyDone && pending === 0`, whichever event lands last (the zero-pending branch still covers the fields-only #B93 terminal), and a mid-stream write error (missing dir, disk full) gets a guarded 500 instead of the historical unhandled-'error' uncaughtException SIGTERM. Downstream, `Controller.store()`'s mover streams each file to a temp sibling and publishes with an ATOMIC rename (a reader never observes a partial file; a pre-existing destination is replaced only on success), propagates the REAL filesystem Error (previously masked as `No file to upload`), and never settles the callback twice (#B223). Since #B227 the SAME semantics govern the general-purpose byte-writer in `helpers/path.js` behind `_().cp()` / `PathObject.mv()` (which CLI copy/build/rename paths ride): temp sibling in the destination's own directory + atomic rename (a pre-existing destination is no longer unlinked BEFORE the copy — its content survives a failed copy), a source-stream error listener (was an unhandled `error` event → process kill), a settled latch (a destination-side failure previously settled twice, the second time as a success, forking `browseCopy`'s directory recursion), and a real `Error` instead of the former plain string (`Error on Path.cp(...): Not found ...`) — so caller `err.stack` prints stop logging `undefined`. The destination mkdir is crash-guarded (#B145): a group whose custom `path` has a read-only/EACCES/EROFS parent answers a guarded 500 naming group + path — a server CONFIG problem is 500 (the #B50 unconfigured-group 400 stays upstream and unchanged), and pre-fix `fs.mkdirSync` threw SYNCHRONOUSLY inside the parser callback → uncaughtException → SIGTERM, an unauthenticated single-request bundle kill (the multipart parse precedes routing and all middleware, both engines). A per-group `simulateWriteError: true` flag (#B144) lets a consumer deterministically fire the guarded-500 write-error path OUTSIDE production scope only (`!NODE_SCOPE_IS_PRODUCTION`): the 'file' handler creates the REAL write stream, arms the REAL terminal listeners, then synthetically `destroy()`s it — the exact terminal semantics of a real ENOSPC/EIO (an errored stream never emits 'finish', so the request stays terminal at the 500), N destroyed parts collapsing to exactly ONE 500 via throwError's `!res.headersSent` guard; a boot warn scans `upload.groups` for the flag in both scopes (production: "IGNORED — remove before shipping"; else "PROBE active"); nothing ships active. The `group="…"` tag rides a Content-Disposition PARAMETER that `curl -F` / browser FormData cannot emit (the `@rhinostone/busboy` fork parses it into `info.dispositionParams.group`), so a faithful probe HAND-BUILDS the multipart body. **The staged upload client layer (`data-gina-form-upload-*`, lives in the validator plugin — browser-bundled ⇒ consumers RE-BAKE at pickup; former #269/#271/#272/#274 — #R8 slices 1-2, #B146/#B147/#B148/#B149):** the staging POST body is assembled as a `Blob` with each File object embedded RAW (#B148 — the historical `FileReader`/`ab2str` DOMString concatenation UTF-8-inflated every byte >= 0x80 on the wire, ×1.49 measured, a PNG's 0x89 arriving as 0xC2 0x89; the corruption only STARTED at 0.5.22 because the pre-#B103 server's two since-removed decode layers EXACTLY reversed the inflation — two wrongs cancelling — so the #B103 server fix is what exposed the client defect). Multipart FRAMING is byte-identical (same boundary delimiters, same `name=`/`group=`/`filename=` disposition-parameter set, values percent-escaped for CR/LF/double-quote per RFC 7578 §5.1.1); a 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); files corrupted by the defect are LOSSLESSLY recoverable (the stored bytes are exactly the UTF-8 encoding of the originals: decode utf8 → re-encode latin1). **Upload progress (#R8 slice 1):** `send()` assigns `xhr.upload.onprogress` FRESH on every send (the module-scoped XHR was reused across sends — a stale handler replays the previous send's closure with the wrong id), dispatching the REGISTERED event `uploadProgress.<uploadFormId>` (the events-array registration is required — `on()` validates names against the plugin registry) with `{ status: 100, progress: <int 0-100 | null when lengthComputable is false>, loaded, total, lengthComputable, files: [names] }` — a per-REQUEST aggregate (ONE staging XHR carries every file of a selection; per-file wire progress is not separable). Consumer surfaces: `data-gina-form-upload-on-progress` (bare window identifier, the -on-success convention) + a declarative indicator `data-gina-form-upload-progress="<elId>"` defaulting to `<fieldId>-progress`, opt-in by element presence. The updater feature-detects the target: a native `<progress>` tracks value=loaded/max=total (indeterminate = the value attribute REMOVED for the native animation; error = value 0, NEVER indeterminate — that animation would read as still working); any other element gets percent textContent + `data-gina-upload-progress`/`data-gina-upload-progress-state` styling hooks (preparing|uploading|indeterminate|processing|complete|error) with NO hardcoded wording (i18n-neutral — label via CSS on the state attribute). Lifecycle: `preparing` at selection (covers the FileReader/assembly phase), `uploading` per frame, `processing` at `xhr.upload.onloadend` (the browser finished SENDING — advances the state attribute ONLY, leaving value/percent as the last frame left them so a determinate bar stays visually full), `complete`/`error` finalized in onUpload (one chokepoint covers success and every error/timeout path), reset/delete strips the indicator. The response-side download channel `progress.<id>` is byte-untouched (it also serves attachment downloads). **Drag-and-drop (#R8 slice 2):** a file input carrying `data-gina-form-upload-dropzone="<elementId>"` gets drag listeners bound on the named element at form-bind time; dropped files are assigned to the input (`input.files = dataTransfer.files`) and the input's `change` is re-fired synthetically, so the ENTIRE staging pipeline — group tagging, virtual form, staging POST, previews, hidden metadata fields, reset/delete, upload progress — runs with zero duplicated logic (the change handler reads only `currentTarget`, so a synthetic dispatch is indistinguishable from a trusted one on this path). Contract: EXPLICIT-id-only (deliberately NO `<fieldId>-dropzone` default — auto-binding a coincidentally-named element would attach drag semantics to markup that may carry its own drop handling; absent attribute = inert, missing element = console.warn + inert); the zone is stamped `data-gina-upload-dropzone` (value = owner input id — the first-wins guard: one zone serves one input) and `data-gina-upload-dropzone-state` (`idle` → `over` on a file-drag hover, DEPTH-COUNTED so child-boundary crossings never flicker → `dropped` → back to `idle` at the same onUpload chokepoint that finalizes progress, and at reset/delete) — pure CSS hooks, no hardcoded wording. Only FILE drags react (`dataTransfer.types` must carry `Files`; text/link drags fall through untouched, never preventDefault'd); a multi-file drop on a non-`multiple` input keeps the FIRST file only (console.warn) — configured groups still enforce `isMultipleAllowed` server-side; a bare file input already accepts native browser drops through the same change handler — the attribute exists to delegate a larger/styled element. **Action/preview/bind fixes (#B146/#B147/#B149):** `checkUploadUrlActions`'s default-route fallback writes the attribute ACTUALLY being checked (`setAttribute(action, …)` — it used to hardcode the staging attribute and silently repoint a staging POST at the resolved reset/delete default route, compounding to a SILENT failure when `toUrl()`'s absolute origin tripped a CORS preflight → XHR status-0 → the commented-out status-0 branch); the preview-container guard uses the typeof-null pattern (`&& previewContainer` — `getElementById` returns element|null and `typeof null === 'object'`, so a preview-element MISS used to TypeError the success handler at `.id`); an upload-only input binds QUIETLY — `-delete-action` deliberately has no framework default (it removes an already-saved file, an app-specific endpoint), so a no-default absent action is a single `console.debug` + early return (a WITH-default action that genuinely fails to resolve still errors; the delete requirement stays enforced lazily at `onUploadResetOrDelete`). Tests: `upload-binary-integrity.test.js` / `upload-size-accuracy.test.js` (11) / `multipart-multifile-resume.test.js` (15) / `upload-write-error-probe.test.js` / `upload-config.test.js §07` / `validator-upload-progress.test.js` (46) / `validator-upload-binary-wire.test.js` / `validator-upload-action-guard.test.js` — all red-first-validated with control-gated extractions of the shipped bytes. **The store() fluent form is restored (#B420, 0.6.16):** `self.store(target).onComplete(cb)` returns its `{onComplete}` handle synchronously as documented -- an accidental `async` on the declaration had wrapped the handle in a Promise on every stable from v0.6.0 to v0.6.15, so the fluent form (the upload guide entry-point example) threw `TypeError` while the 3-arg callback form worked throughout; the body was await-free, nothing anywhere awaits `store()`, and dropping the keyword is the whole fix (types now declare the fluent return shape).
904
904
 
@@ -1038,3 +1038,5 @@ Dev-mode query instrumentation captures every database query tied to the current
1038
1038
 
1039
1039
  318. **Validator AJAX bodies are JSON — the Content-Type must say so, on every sender (#FORMCT; query path fixed in #B425).** The browser validator serializes its request bodies with `JSON.stringify`, but its shared `xhrOptions` defaults hardcode `Content-Type: application/x-www-form-urlencoded; charset=UTF-8` (that literal appears exactly once in the whole shipped bundle). When a JSON body goes out under that label the gina server correctly honours it and runs `body.replace(/\+/g, ' ')` plus `decodeURIComponent` BEFORE `JSON.parse` — so a value such as an email `+alias` arrives with the `+` turned into a space. **The failure is silent by construction:** the mangled body is still well-formed JSON, so it parses, no error is raised, and the endpoint answers `200` with a wrong answer. The server half is NOT the bug — for a genuine urlencoded body `+` really does mean space and `%2B` survives the decode — so every fix belongs on the client, at the labelling. **Rule for any new sender: if the body is JSON, resolve the Content-Type as `explicit author declaration, else application/json` — never let the urlencoded default ride a JSON body.** The senders and their state: the form-submit non-binary path and the file-removal POST resolve it correctly (they read an explicit form `enctype` first, else `application/json`); the multipart/binary path sets its own boundary header; a bodiless branch sets a header but sends nothing, so nothing can be corrupted; and the `query` (live-check) request in `core/plugins/lib/validator/src/form-validator.js` was the last one still inheriting the default — fixed by mirroring the same resolution. **Implementation gotcha specific to the query path:** the rule author can override the header per rule (`"headers": { "Content-Type": "application/json" }` inside a rule's `query` block, a supported seam), and that override must keep winning — but the options merge folds the rule block and the defaults into one object, after which a rule-declared `urlencoded` and the inherited default are the same string in the same slot. Provenance is unrecoverable post-merge, so the rule-declared value must be captured BEFORE the merge and consulted afterwards. **Behaviour note at pickup:** live-check bodies now route to the server's parse-verbatim JSON branch, so `+` survives — and `%XX` sequences are no longer URL-decoded either. The client stringifies raw field values without percent-encoding, so no correct consumer depends on that decode, but a bundle that leaned on it will see the literal. **Merge semantics this relies on (measured, and undocumented in the merge JSDoc):** `lib/merge` is variadic — it reads `override` off the LAST argument only when that argument is a boolean, and treats every other argument as an additional source, with EARLIER sources winning and later ones filling gaps. That is why a per-rule header beats the default, and why passing a third object is a merge rather than an override flag.
1040
1040
  319. **Controller render data — the dotted-path `set()` contract, and why a first write must never be walked (#P39 slice 2).** Inside `core/controller/controller.js`, every render is preceded by ~60 calls to a local `set('a.b.c', value)` that builds `local.userData` (`page.view.*`, `page.environment.*`, `page.forms`, `gina.csrfToken`, …). Its contract: missing intermediates are created as plain objects and an intermediate that is not a plain object (`null`, an array, a primitive) is REPLACED; at the leaf, **first write wins** (a later `set()` to an existing leaf is ignored — the `override` argument is accepted but inert), an object written onto an existing object leaf **deep-fills** it (missing keys added, existing kept) through `lib/merge`, and **a first-write value is stored by reference and is never walked, copied or mutated**. That last clause is the load-bearing one: `set('page.forms', conf.content.forms)` stores the shared per-process forms catalog, and the previous implementation — a JSON-string path builder + `JSON.parse` + a whole-subtree `merge` — recursed into every grafted object, re-merged each array inside it with itself, and wrote the copies back into the caller's object: 0.4–1.5 ms of CPU per request for a real catalog, and the catalog's arrays silently swapped for equal copies on every render. (That lib-level rewrite of a grafted source — array identity churn at every depth plus SILENT PRIMITIVE DEDUPE — was #B428, fixed in 0.6.19: `lib/merge` now skips a subtree that is the SAME object on both sides — merging a value into itself is identity — so a grafted source keeps its arrays and duplicates and the result references it; object/array references only, equal primitives keep their path; the root-absent `createMode` shallow-copy path is deliberately unchanged. Measured on a 2000-case seeded corpus: identical results and untouched sources for every non-shared merge. Two by-catches were staked out of it: #B436 — FIXED in 0.6.20: staked as 'nested arrays at depth ≥ 2 merge TWICE' (`[1,2,3,4,3,4]`), the MEASURED root was `mergeArray`'s number top-up deciding 'push again' by INDEX rather than by COUNT — any number the target already held at a shifted index was pushed again at EVERY depth (`merge([9,1],[1])` → `[9,1,1]`; `{ports:[8080,8124]}`+`{ports:[8124]}` → `[8080,8124,8124]`), and the depth-≥2 double pass (`browse()` merged arrays in its prop loop and again in the recursion) merely manufactured the shift; the top-up now compares counts (`[25]`+`[25,25]` still `[25,25]`, a present value is never pushed again, merging the same source twice is a no-op) and the prop-loop array merge runs only on the createMode path, so a nested array is merged once and an array SHARED by both sides at depth ≥ 2 keeps its identity (a #B428 residual); #B437 — FIXED in 0.6.20: a target array repeating a primitive (`['v2','v2']`) merged with an array of objects threw `TypeError: Cannot create property 'id' on string`, because `mergeArray`'s object-fill branch tested for a free index on the DEDUPED rebuild of the target and then wrote into the real target, whose index still held the string (or `null`); an occupied index is now left alone, as it already was for an object landing on one.) The rewrite assigns a first write directly and routes only genuine collisions through `merge` at the leaf, so the observable structure is identical (11,550 states differential-tested against the frozen old implementation) while the catalog is left alone; measured ×4.5 on the primitive writes and ×800+ on the catalog write. **Two generalisable traps from that work.** (1) Until 0.6.19 `lib/merge` MUTATED A SOURCE IT GRAFTED: a source subtree grafted by reference at an existing level was walked by the recursion with `src === copy`, every array inside it re-merged with itself and the copies written back into the caller's object — contents preserved, identity churned, cost proportional to the value. #B428's identity guard closed it, and the lesson stands for any merge-like helper that walks a caller's object by reference: measure identity through it before handing it an object you must keep intact. (2) `lib/merge` is VARIADIC and its copy behaviour depends on depth and shape: a value at depth 1 of a call is shallow-copied, deeper values are referenced (and until 0.6.20 a nested array was merged twice on the same level — once in the graft loop, once in the recursion). The array contract is now in its JSDoc (#B436); still read `browse()` before reasoning about identity through merge, and prefer a measured differential test over a model of it.
1041
+ 320. **`route.request()` settles its caller exactly once - an outbound-request failure is an error argument, never a hang and never a bundle kill (#B442, 0.6.21).** The routing helper's outbound call (`getRoute(...).request([ignoreWebRoot,] options[, cb])`) DISCARDED the `ClientRequest` returned by `http(s).get`, so nothing could ever attach an `'error'` listener to it. Three consequences, all removed: (a) a dial failure whose code is carved out of the process-level uncaughtException handler (`ECONNREFUSED`/`ECONNRESET`/`EPIPE`/...) left the caller waiting FOREVER with nothing logged - a permanent wedge in any serialized consumer (a worker at concurrency 1, a queue processor), and the SILENCE is the defect, not the carve-out; (b) a code that is NOT carved out (`ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH` - i.e. a peer that stops answering or a route that goes away) escalated to `uncaughtException` and took the bundle down through the process-level `dismiss(pid, 'SIGTERM')`; (c) a response stream dying mid-body assigned its error to a local and waited for an `'end'` event that never arrives, so the assigned error was never delivered either. All four exit paths now run through ONE single-settle latch (`settled` flag + `settle()`), so the callback receives an ordinary error argument - the shape consuming code already handles - and can never be invoked twice. **`options.timeout` was INERT and looked like a consumer workaround:** it does reach `http(s).get`, but Node only EMITS `'timeout'` and never destroys the socket, and the request object was unreachable, so nobody could listen or act. It is now honoured - on expiry the request is destroyed and the callback settles with an `Error` carrying `code === 'ETIMEDOUT'`; absent/falsy `options.timeout` attaches no timeout listener, so the behaviour is opt-in. The no-callback (fire-and-forget) branch attaches its own `'error'` listener and `console.warn`s, so a call nobody awaited can no longer kill the process. Same defect class as the `store()` / `copyFile` movers (#B223/#B227): capture every request or stream you create, listen on BOTH ends, and latch the settle. **`lib/routing` IS browser-bundled** (aliased in the plugin `build.json`), so this ships in `gina.min.js` as well as server-side - pickup is a bundle restart AND a re-bake. Regression test: `test/lib/routing-request-settle.test.js` - source pins plus extract-and-execute behavioural arms driving the real shipped bytes against a fake agent; note the function region is brace-UNBALANCED (a `/^\{/` regex literal in the end handler), so the extraction is terminator-anchored on `} // EO route.request()` and a brace walker must NOT be substituted for it.
1042
+ 321. **`merge()` array-element hardening - a null (or hole) element in an id-keyed array is an element with no key, never a crash (#B436/#B437/#B443 arc).** `typeof null == 'object'`, so `mergeArray`'s collection paths historically dereferenced null elements and threw `TypeError: Cannot read properties of null` - and because every configuration overlay and collection merge travels through `lib/merge`, any such shape was a whole-merge (often whole-boot) kill. The hardened contract, in layers: the WRITE side (0.6.20) - the index-merge branch tests for a free slot on the real target, and an occupied slot (a primitive or null left where the deduped rebuild had a hole) is skipped, never written into; number top-up is by COUNT, not index, so merges are idempotent (`[25]+[25,25]` gives `[25,25]`, `[9,1]+[1]` stays `[9,1]`). The READ side (0.6.21) - both collection ENTRY guards null-check the TARGET operand (they already checked the source), both id-ROSTER loops and both WALKS skip a null element, and the per-element collection tests null-check the SOURCE operand; a null element matches nothing, blocks nothing and contributes nothing. Semantics otherwise unchanged and pinned: a target whose slots are primitives/null keeps them (`[null,null]+[{id:1},{id:2}]` returns `[null,null]`, same as `['v2','v2']+...`), and an object BEHIND a primitive/null head is dropped by the per-element branch (pre-existing, `['x',{id:9}]+[{id:1}]` returns `['x']`). `lib/merge` is browser-bundled (aliased in the plugin `build.json`), so array-merge fixes ship in `gina.min.js` too - pickup is a bundle restart AND a re-bake. Regression suites: `test/lib/merge-b436.test.js`, `merge-b437.test.js`, `merge-b443.test.js` (behavioural arms driving the real lib + comment-stripped source pins + artifact-derived dist pins, all red-first validated).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gina",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
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.20/core/gna",
79
+ "main": "./framework/v0.6.21/core/gna",
80
80
  "exports": {
81
81
  ".": {
82
82
  "types": "./types/index.d.ts",
83
83
  "import": "./index.mjs",
84
- "require": "./framework/v0.6.20/core/gna.js"
84
+ "require": "./framework/v0.6.21/core/gna.js"
85
85
  },
86
86
  "./gna": {
87
87
  "types": "./types/gna.d.ts",
@@ -129,7 +129,7 @@
129
129
  "jsdom": "^30.0.1",
130
130
  "nunjucks": "^3.2.4",
131
131
  "requirejs": "^2.3.7",
132
- "sass": "1.102.0",
132
+ "sass": "1.103.1",
133
133
  "typescript": "^7.0.2"
134
134
  },
135
135
  "overrides": {
@@ -1 +0,0 @@
1
- 0.6.20