gina 0.7.2 → 0.7.3

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 (633) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +40 -40
  3. package/ROADMAP.md +1 -0
  4. package/bin/cli +29 -0
  5. package/bin/cmd +34 -25
  6. package/bin/gina +55 -15
  7. package/framework/v0.7.3/VERSION +1 -0
  8. package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/js/gina.js +44 -8
  9. package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/js/gina.min.js +602 -602
  10. package/framework/v0.7.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  11. package/framework/v0.7.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  12. package/framework/{v0.7.2 → v0.7.3}/core/config.js +5 -0
  13. package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.js +39 -6
  14. package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-swig.js +14 -1
  15. package/framework/v0.7.3/core/controller/preload-hints.js +230 -0
  16. package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/validator/src/main.js +26 -4
  17. package/framework/{v0.7.2 → v0.7.3}/core/server.js +181 -19
  18. package/framework/{v0.7.2 → v0.7.3}/core/template/conf/templates.json +2 -1
  19. package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/add.js +1 -1
  20. package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/help.txt +6 -5
  21. package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/help.txt +5 -1
  22. package/framework/v0.7.3/lib/cmd/framework/inc/listen-error.js +217 -0
  23. package/framework/v0.7.3/lib/cmd/framework/inc/ps-titles.js +193 -0
  24. package/framework/{v0.7.2 → v0.7.3}/lib/cmd/gina-framework.1.md +2 -0
  25. package/framework/{v0.7.2 → v0.7.3}/lib/cmd/gina.1.md +4 -0
  26. package/framework/{v0.7.2 → v0.7.3}/package.json +1 -1
  27. package/gna.js +4 -4
  28. package/llms.txt +5 -5
  29. package/package.json +3 -3
  30. package/schema/settings.json +5 -0
  31. package/types/index.d.ts +1 -0
  32. package/utils/helper.js +33 -0
  33. package/framework/v0.7.2/VERSION +0 -1
  34. package/framework/v0.7.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  35. package/framework/v0.7.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  36. package/framework/v0.7.2/lib/cmd/framework/inc/ps-titles.js +0 -113
  37. /package/framework/{v0.7.2 → v0.7.3}/AUTHORS +0 -0
  38. /package/framework/{v0.7.2 → v0.7.3}/LICENSE +0 -0
  39. /package/framework/{v0.7.2 → v0.7.3}/core/asset/html/nolayout.html +0 -0
  40. /package/framework/{v0.7.2 → v0.7.3}/core/asset/html/static.html +0 -0
  41. /package/framework/{v0.7.2 → v0.7.3}/core/asset/img/android-chrome-192x192.png +0 -0
  42. /package/framework/{v0.7.2 → v0.7.3}/core/asset/img/android-chrome-512x512.png +0 -0
  43. /package/framework/{v0.7.2 → v0.7.3}/core/asset/img/apple-touch-icon.png +0 -0
  44. /package/framework/{v0.7.2 → v0.7.3}/core/asset/img/favicon-16x16.png +0 -0
  45. /package/framework/{v0.7.2 → v0.7.3}/core/asset/img/favicon-32x32.png +0 -0
  46. /package/framework/{v0.7.2 → v0.7.3}/core/asset/img/favicon.ico +0 -0
  47. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/README.md +0 -0
  48. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
  49. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
  50. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
  51. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -0
  52. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
  53. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
  54. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +0 -0
  55. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
  56. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
  57. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
  58. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/inspector/index.html +0 -0
  59. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +0 -0
  60. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +0 -0
  61. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
  62. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
  63. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
  64. /package/framework/{v0.7.2 → v0.7.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
  65. /package/framework/{v0.7.2 → v0.7.3}/core/config.requirements-anchor.js +0 -0
  66. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/ai/index.js +0 -0
  67. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/ai/lib/connector.js +0 -0
  68. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/index.js +0 -0
  69. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/connector.js +0 -0
  70. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/connector.v3.js +0 -0
  71. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/connector.v4.js +0 -0
  72. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/n1ql.js +0 -0
  73. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/session-store.js +0 -0
  74. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/session-store.v3.js +0 -0
  75. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/session-store.v4.js +0 -0
  76. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/couchbase/lib/storage-store.js +0 -0
  77. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/duckdb/index.js +0 -0
  78. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/duckdb/lib/connector.js +0 -0
  79. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mongodb/index.js +0 -0
  80. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mongodb/lib/connector.js +0 -0
  81. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mongodb/lib/job-store.js +0 -0
  82. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
  83. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mongodb/lib/session-store.js +0 -0
  84. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mysql/index.js +0 -0
  85. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/mysql/lib/connector.js +0 -0
  86. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/param-redact.js +0 -0
  87. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/postgresql/index.js +0 -0
  88. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/postgresql/lib/connector.js +0 -0
  89. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/redis/index.js +0 -0
  90. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/redis/lib/connector.js +0 -0
  91. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/redis/lib/job-store.js +0 -0
  92. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/redis/lib/kv-store.js +0 -0
  93. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/redis/lib/render-cache-store.js +0 -0
  94. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/redis/lib/session-store.js +0 -0
  95. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/scylladb/index.js +0 -0
  96. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/scylladb/lib/connector.js +0 -0
  97. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/scylladb/lib/session-store.js +0 -0
  98. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/settle-once.js +0 -0
  99. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/sql-parser.js +0 -0
  100. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/sqlite/index.js +0 -0
  101. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/sqlite/lib/connector.js +0 -0
  102. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/sqlite/lib/job-store.js +0 -0
  103. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/sqlite/lib/kv-store.js +0 -0
  104. /package/framework/{v0.7.2 → v0.7.3}/core/connectors/sqlite/lib/session-store.js +0 -0
  105. /package/framework/{v0.7.2 → v0.7.3}/core/content.encoding +0 -0
  106. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.framework.js +0 -0
  107. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-json.js +0 -0
  108. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-nunjucks-async.js +0 -0
  109. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-nunjucks.js +0 -0
  110. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-stream.js +0 -0
  111. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-swig-async.js +0 -0
  112. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-v1.js +0 -0
  113. /package/framework/{v0.7.2 → v0.7.3}/core/controller/controller.render-xml.js +0 -0
  114. /package/framework/{v0.7.2 → v0.7.3}/core/controller/index.js +0 -0
  115. /package/framework/{v0.7.2 → v0.7.3}/core/controller/inline-script.js +0 -0
  116. /package/framework/{v0.7.2 → v0.7.3}/core/controller/inspector-window-emit.js +0 -0
  117. /package/framework/{v0.7.2 → v0.7.3}/core/controller/release-banner.js +0 -0
  118. /package/framework/{v0.7.2 → v0.7.3}/core/dev/index.js +0 -0
  119. /package/framework/{v0.7.2 → v0.7.3}/core/dev/lib/class.js +0 -0
  120. /package/framework/{v0.7.2 → v0.7.3}/core/dev/lib/factory.js +0 -0
  121. /package/framework/{v0.7.2 → v0.7.3}/core/dev/lib/tools.js +0 -0
  122. /package/framework/{v0.7.2 → v0.7.3}/core/gna.js +0 -0
  123. /package/framework/{v0.7.2 → v0.7.3}/core/locales/README.md +0 -0
  124. /package/framework/{v0.7.2 → v0.7.3}/core/locales/currency.json +0 -0
  125. /package/framework/{v0.7.2 → v0.7.3}/core/locales/dist/language/en.json +0 -0
  126. /package/framework/{v0.7.2 → v0.7.3}/core/locales/dist/language/fr.json +0 -0
  127. /package/framework/{v0.7.2 → v0.7.3}/core/locales/dist/region/en.json +0 -0
  128. /package/framework/{v0.7.2 → v0.7.3}/core/locales/dist/region/fr.json +0 -0
  129. /package/framework/{v0.7.2 → v0.7.3}/core/locales/index.js +0 -0
  130. /package/framework/{v0.7.2 → v0.7.3}/core/mime.types +0 -0
  131. /package/framework/{v0.7.2 → v0.7.3}/core/model/entity.js +0 -0
  132. /package/framework/{v0.7.2 → v0.7.3}/core/model/index.js +0 -0
  133. /package/framework/{v0.7.2 → v0.7.3}/core/model/template/entityFactory.js +0 -0
  134. /package/framework/{v0.7.2 → v0.7.3}/core/model/template/index.js +0 -0
  135. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/README.md +0 -0
  136. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/index.js +0 -0
  137. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/csrf/README.md +0 -0
  138. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/csrf/package.json +0 -0
  139. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/csrf/src/main.js +0 -0
  140. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/README.md +0 -0
  141. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/coep/README.md +0 -0
  142. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/coep/package.json +0 -0
  143. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
  144. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/coop/README.md +0 -0
  145. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/coop/package.json +0 -0
  146. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
  147. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/corp/README.md +0 -0
  148. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/corp/package.json +0 -0
  149. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
  150. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/csp/README.md +0 -0
  151. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/csp/package.json +0 -0
  152. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
  153. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/hide-powered-by/README.md +0 -0
  154. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
  155. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
  156. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/hsts/README.md +0 -0
  157. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/hsts/package.json +0 -0
  158. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
  159. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
  160. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
  161. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
  162. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/package.json +0 -0
  163. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
  164. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
  165. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
  166. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/src/main.js +0 -0
  167. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
  168. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
  169. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
  170. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
  171. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
  172. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
  173. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
  174. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
  175. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
  176. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
  177. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
  178. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
  179. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
  180. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
  181. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
  182. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
  183. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
  184. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
  185. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/session/README.md +0 -0
  186. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/session/package.json +0 -0
  187. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/session/src/main.js +0 -0
  188. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/storage/README.md +0 -0
  189. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/storage/build.json +0 -0
  190. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/storage/package.json +0 -0
  191. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/storage/src/main.js +0 -0
  192. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/validator/README.md +0 -0
  193. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/validator/build.json +0 -0
  194. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/validator/package.json +0 -0
  195. /package/framework/{v0.7.2 → v0.7.3}/core/plugins/lib/validator/src/form-validator.js +0 -0
  196. /package/framework/{v0.7.2 → v0.7.3}/core/router.js +0 -0
  197. /package/framework/{v0.7.2 → v0.7.3}/core/server.express.js +0 -0
  198. /package/framework/{v0.7.2 → v0.7.3}/core/server.isaac.js +0 -0
  199. /package/framework/{v0.7.2 → v0.7.3}/core/server.isaac.rapid-reset.js +0 -0
  200. /package/framework/{v0.7.2 → v0.7.3}/core/server.route-candidates.js +0 -0
  201. /package/framework/{v0.7.2 → v0.7.3}/core/status.codes +0 -0
  202. /package/framework/{v0.7.2 → v0.7.3}/core/template/_gitignore +0 -0
  203. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/app.json +0 -0
  204. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/connectors.json +0 -0
  205. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/routing.json +0 -0
  206. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/settings.json +0 -0
  207. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
  208. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/templates.json +0 -0
  209. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/config/watchers.json +0 -0
  210. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
  211. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
  212. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
  213. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/index.js +0 -0
  214. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle/locales/en.json +0 -0
  215. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
  216. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/css/default.css +0 -0
  217. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/css/home.css +0 -0
  218. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
  219. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
  220. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
  221. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
  222. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
  223. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/readme.md +0 -0
  224. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_public/sw.js +0 -0
  225. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
  226. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
  227. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
  228. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
  229. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
  230. /package/framework/{v0.7.2 → v0.7.3}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
  231. /package/framework/{v0.7.2 → v0.7.3}/core/template/command/gina.bat.tpl +0 -0
  232. /package/framework/{v0.7.2 → v0.7.3}/core/template/command/gina.tpl +0 -0
  233. /package/framework/{v0.7.2 → v0.7.3}/core/template/conf/env.json +0 -0
  234. /package/framework/{v0.7.2 → v0.7.3}/core/template/conf/manifest.json +0 -0
  235. /package/framework/{v0.7.2 → v0.7.3}/core/template/conf/package.json +0 -0
  236. /package/framework/{v0.7.2 → v0.7.3}/core/template/conf/settings.json +0 -0
  237. /package/framework/{v0.7.2 → v0.7.3}/core/template/conf/statics.json +0 -0
  238. /package/framework/{v0.7.2 → v0.7.3}/core/template/error/client/json/401.json +0 -0
  239. /package/framework/{v0.7.2 → v0.7.3}/core/template/error/client/json/403.json +0 -0
  240. /package/framework/{v0.7.2 → v0.7.3}/core/template/error/client/json/404.json +0 -0
  241. /package/framework/{v0.7.2 → v0.7.3}/core/template/error/server/html/50x.html +0 -0
  242. /package/framework/{v0.7.2 → v0.7.3}/core/template/error/server/json/500.json +0 -0
  243. /package/framework/{v0.7.2 → v0.7.3}/core/template/error/server/json/503.json +0 -0
  244. /package/framework/{v0.7.2 → v0.7.3}/core/template/extensions/logger/config.json +0 -0
  245. /package/framework/{v0.7.2 → v0.7.3}/helpers/console.js +0 -0
  246. /package/framework/{v0.7.2 → v0.7.3}/helpers/context.js +0 -0
  247. /package/framework/{v0.7.2 → v0.7.3}/helpers/data/LICENSE +0 -0
  248. /package/framework/{v0.7.2 → v0.7.3}/helpers/data/README.md +0 -0
  249. /package/framework/{v0.7.2 → v0.7.3}/helpers/data/package.json +0 -0
  250. /package/framework/{v0.7.2 → v0.7.3}/helpers/data/src/main.js +0 -0
  251. /package/framework/{v0.7.2 → v0.7.3}/helpers/dateFormat.js +0 -0
  252. /package/framework/{v0.7.2 → v0.7.3}/helpers/index.js +0 -0
  253. /package/framework/{v0.7.2 → v0.7.3}/helpers/json/LICENSE +0 -0
  254. /package/framework/{v0.7.2 → v0.7.3}/helpers/json/README.md +0 -0
  255. /package/framework/{v0.7.2 → v0.7.3}/helpers/json/package.json +0 -0
  256. /package/framework/{v0.7.2 → v0.7.3}/helpers/json/src/main.js +0 -0
  257. /package/framework/{v0.7.2 → v0.7.3}/helpers/path.js +0 -0
  258. /package/framework/{v0.7.2 → v0.7.3}/helpers/plugins/README.md +0 -0
  259. /package/framework/{v0.7.2 → v0.7.3}/helpers/plugins/package.json +0 -0
  260. /package/framework/{v0.7.2 → v0.7.3}/helpers/plugins/src/api-error.js +0 -0
  261. /package/framework/{v0.7.2 → v0.7.3}/helpers/plugins/src/main.js +0 -0
  262. /package/framework/{v0.7.2 → v0.7.3}/helpers/prototypes.js +0 -0
  263. /package/framework/{v0.7.2 → v0.7.3}/helpers/task.js +0 -0
  264. /package/framework/{v0.7.2 → v0.7.3}/helpers/text.js +0 -0
  265. /package/framework/{v0.7.2 → v0.7.3}/lib/admin/package.json +0 -0
  266. /package/framework/{v0.7.2 → v0.7.3}/lib/admin/src/main.js +0 -0
  267. /package/framework/{v0.7.2 → v0.7.3}/lib/archiver/README.md +0 -0
  268. /package/framework/{v0.7.2 → v0.7.3}/lib/archiver/build.json +0 -0
  269. /package/framework/{v0.7.2 → v0.7.3}/lib/archiver/package.json +0 -0
  270. /package/framework/{v0.7.2 → v0.7.3}/lib/archiver/src/dep/jszip.min.js +0 -0
  271. /package/framework/{v0.7.2 → v0.7.3}/lib/archiver/src/main.js +0 -0
  272. /package/framework/{v0.7.2 → v0.7.3}/lib/async/package.json +0 -0
  273. /package/framework/{v0.7.2 → v0.7.3}/lib/async/src/main.js +0 -0
  274. /package/framework/{v0.7.2 → v0.7.3}/lib/audit/package.json +0 -0
  275. /package/framework/{v0.7.2 → v0.7.3}/lib/audit/src/main.js +0 -0
  276. /package/framework/{v0.7.2 → v0.7.3}/lib/audit-store.js +0 -0
  277. /package/framework/{v0.7.2 → v0.7.3}/lib/authn/package.json +0 -0
  278. /package/framework/{v0.7.2 → v0.7.3}/lib/authn/src/lockout.js +0 -0
  279. /package/framework/{v0.7.2 → v0.7.3}/lib/authn/src/main.js +0 -0
  280. /package/framework/{v0.7.2 → v0.7.3}/lib/authn/src/totp.js +0 -0
  281. /package/framework/{v0.7.2 → v0.7.3}/lib/authz-gate/package.json +0 -0
  282. /package/framework/{v0.7.2 → v0.7.3}/lib/authz-gate/src/main.js +0 -0
  283. /package/framework/{v0.7.2 → v0.7.3}/lib/cache/README.md +0 -0
  284. /package/framework/{v0.7.2 → v0.7.3}/lib/cache/build.json +0 -0
  285. /package/framework/{v0.7.2 → v0.7.3}/lib/cache/package.json +0 -0
  286. /package/framework/{v0.7.2 → v0.7.3}/lib/cache/src/main.js +0 -0
  287. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/aliases.json +0 -0
  288. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/audit/arguments.json +0 -0
  289. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/audit/help.txt +0 -0
  290. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/audit/verify.js +0 -0
  291. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/add.js +0 -0
  292. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/arguments.json +0 -0
  293. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/build.js +0 -0
  294. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/copy.js +0 -0
  295. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/cp.js +0 -0
  296. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/help.js +0 -0
  297. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/help.txt +0 -0
  298. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/inc/boot-lines.js +0 -0
  299. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
  300. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/inc/name.js +0 -0
  301. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/list.js +0 -0
  302. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/man.js +0 -0
  303. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/mcp-start.js +0 -0
  304. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/mcp.js +0 -0
  305. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/oas.js +0 -0
  306. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/openapi.js +0 -0
  307. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/remove.js +0 -0
  308. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/rename.js +0 -0
  309. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/restart.js +0 -0
  310. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/rm.js +0 -0
  311. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/start.js +0 -0
  312. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/status.js +0 -0
  313. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/stop.js +0 -0
  314. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/bundle/types.js +0 -0
  315. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/cache/arguments.json +0 -0
  316. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/cache/clear.js +0 -0
  317. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/cache/help.txt +0 -0
  318. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/cache/stats.js +0 -0
  319. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/arguments.json +0 -0
  320. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/help.js +0 -0
  321. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/infer.js +0 -0
  322. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/list.js +0 -0
  323. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/migrate.js +0 -0
  324. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/models.js +0 -0
  325. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/remove.js +0 -0
  326. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/rm.js +0 -0
  327. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/connector/test.js +0 -0
  328. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/container/arguments.json +0 -0
  329. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/container/help.js +0 -0
  330. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/container/help.txt +0 -0
  331. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/container/man.js +0 -0
  332. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/container/ps.js +0 -0
  333. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/container/stop.js +0 -0
  334. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/add.js +0 -0
  335. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/arguments.json +0 -0
  336. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/help.txt +0 -0
  337. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/inc/args.js +0 -0
  338. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/inc/namespace.js +0 -0
  339. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
  340. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/inc/reference-scan.js +0 -0
  341. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/inc/scaffold.js +0 -0
  342. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/remove.js +0 -0
  343. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/rename.js +0 -0
  344. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/controller/rm.js +0 -0
  345. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/add.js +0 -0
  346. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/get.js +0 -0
  347. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/help.js +0 -0
  348. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/help.txt +0 -0
  349. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/inc/name.js +0 -0
  350. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/link-dev.js +0 -0
  351. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/list.js +0 -0
  352. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/remove.js +0 -0
  353. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/rm.js +0 -0
  354. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/set.js +0 -0
  355. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/unset.js +0 -0
  356. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/env/use.js +0 -0
  357. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/add.js +0 -0
  358. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/arguments.json +0 -0
  359. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/build.js +0 -0
  360. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/dot.js +0 -0
  361. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/get.js +0 -0
  362. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/help.js +0 -0
  363. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/init.js +0 -0
  364. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/link-node-modules.js +0 -0
  365. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/link.js +0 -0
  366. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/list.js +0 -0
  367. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/man.js +0 -0
  368. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/msg.json +0 -0
  369. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/open.js +0 -0
  370. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/remove.js +0 -0
  371. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/reset.js +0 -0
  372. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/restart.js +0 -0
  373. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/set.js +0 -0
  374. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/start.js +0 -0
  375. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/status.js +0 -0
  376. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/stop.js +0 -0
  377. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/tail.js +0 -0
  378. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/update.js +0 -0
  379. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/framework/version.js +0 -0
  380. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/gina-dev.1.md +0 -0
  381. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/helper.js +0 -0
  382. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/add.js +0 -0
  383. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/arguments.json +0 -0
  384. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/export.js +0 -0
  385. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/help.js +0 -0
  386. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/help.txt +0 -0
  387. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/import.js +0 -0
  388. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/i18n/scan.js +0 -0
  389. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/_host.js +0 -0
  390. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/arguments.json +0 -0
  391. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/build.js +0 -0
  392. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/help.js +0 -0
  393. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/help.txt +0 -0
  394. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/list.js +0 -0
  395. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/man.js +0 -0
  396. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/rm.js +0 -0
  397. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/image/run.js +0 -0
  398. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/index.js +0 -0
  399. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/inspector/help.js +0 -0
  400. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/inspector/help.txt +0 -0
  401. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/inspector/open.js +0 -0
  402. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/man-render.js +0 -0
  403. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/minion/arguments.json +0 -0
  404. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/minion/help.js +0 -0
  405. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/minion/help.txt +0 -0
  406. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/minion/kill.js +0 -0
  407. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/minion/list.js +0 -0
  408. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/msg.json +0 -0
  409. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/port/help.js +0 -0
  410. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/port/help.txt +0 -0
  411. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/port/inc/scan.js +0 -0
  412. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/port/list.js +0 -0
  413. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/port/reset.js +0 -0
  414. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/port/set.js +0 -0
  415. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/add.js +0 -0
  416. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/arguments.json +0 -0
  417. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/backup.js +0 -0
  418. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/build.js +0 -0
  419. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/help.js +0 -0
  420. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/help.txt +0 -0
  421. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/import.js +0 -0
  422. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/inc/name.js +0 -0
  423. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/list.js +0 -0
  424. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/man.js +0 -0
  425. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/move.js +0 -0
  426. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/remove.js +0 -0
  427. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/rename.js +0 -0
  428. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/restart.js +0 -0
  429. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/restore.js +0 -0
  430. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/rm.js +0 -0
  431. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/start.js +0 -0
  432. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/status.js +0 -0
  433. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/project/stop.js +0 -0
  434. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/protocol/arguments.json +0 -0
  435. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/protocol/help.js +0 -0
  436. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/protocol/help.txt +0 -0
  437. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/protocol/list.js +0 -0
  438. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/protocol/remove.js +0 -0
  439. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/protocol/set.js +0 -0
  440. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/add.js +0 -0
  441. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/help.js +0 -0
  442. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/help.txt +0 -0
  443. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/inc/name.js +0 -0
  444. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/link-local.js +0 -0
  445. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/link-production.js +0 -0
  446. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/list.js +0 -0
  447. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/remove.js +0 -0
  448. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/rm.js +0 -0
  449. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/scope/use.js +0 -0
  450. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/secrets/arguments.json +0 -0
  451. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/secrets/check.js +0 -0
  452. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/secrets/help.js +0 -0
  453. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/secrets/help.txt +0 -0
  454. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/secrets/scan.js +0 -0
  455. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/service/help.js +0 -0
  456. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/service/help.txt +0 -0
  457. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/service/list.js +0 -0
  458. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/service/man.js +0 -0
  459. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/service/start.js +0 -0
  460. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/storage/arguments.json +0 -0
  461. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/storage/gc.js +0 -0
  462. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/storage/help.txt +0 -0
  463. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/storage/stats.js +0 -0
  464. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/storage/verify.js +0 -0
  465. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd/view/add.js +0 -0
  466. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd-status-format/package.json +0 -0
  467. /package/framework/{v0.7.2 → v0.7.3}/lib/cmd-status-format/src/main.js +0 -0
  468. /package/framework/{v0.7.2 → v0.7.3}/lib/collection/README.md +0 -0
  469. /package/framework/{v0.7.2 → v0.7.3}/lib/collection/build.json +0 -0
  470. /package/framework/{v0.7.2 → v0.7.3}/lib/collection/package.json +0 -0
  471. /package/framework/{v0.7.2 → v0.7.3}/lib/collection/src/main.js +0 -0
  472. /package/framework/{v0.7.2 → v0.7.3}/lib/conf-view/package.json +0 -0
  473. /package/framework/{v0.7.2 → v0.7.3}/lib/conf-view/src/main.js +0 -0
  474. /package/framework/{v0.7.2 → v0.7.3}/lib/config.js +0 -0
  475. /package/framework/{v0.7.2 → v0.7.3}/lib/connector-config/package.json +0 -0
  476. /package/framework/{v0.7.2 → v0.7.3}/lib/connector-config/src/main.js +0 -0
  477. /package/framework/{v0.7.2 → v0.7.3}/lib/connector-error/package.json +0 -0
  478. /package/framework/{v0.7.2 → v0.7.3}/lib/connector-error/src/main.js +0 -0
  479. /package/framework/{v0.7.2 → v0.7.3}/lib/connector-registry/package.json +0 -0
  480. /package/framework/{v0.7.2 → v0.7.3}/lib/connector-registry/src/main.js +0 -0
  481. /package/framework/{v0.7.2 → v0.7.3}/lib/cron/README.md +0 -0
  482. /package/framework/{v0.7.2 → v0.7.3}/lib/cron/package.json +0 -0
  483. /package/framework/{v0.7.2 → v0.7.3}/lib/cron/src/main.js +0 -0
  484. /package/framework/{v0.7.2 → v0.7.3}/lib/domain/LICENSE +0 -0
  485. /package/framework/{v0.7.2 → v0.7.3}/lib/domain/README.md +0 -0
  486. /package/framework/{v0.7.2 → v0.7.3}/lib/domain/package.json +0 -0
  487. /package/framework/{v0.7.2 → v0.7.3}/lib/domain/src/main.js +0 -0
  488. /package/framework/{v0.7.2 → v0.7.3}/lib/dto/package.json +0 -0
  489. /package/framework/{v0.7.2 → v0.7.3}/lib/dto/src/main.js +0 -0
  490. /package/framework/{v0.7.2 → v0.7.3}/lib/dto-pipe/package.json +0 -0
  491. /package/framework/{v0.7.2 → v0.7.3}/lib/dto-pipe/src/main.js +0 -0
  492. /package/framework/{v0.7.2 → v0.7.3}/lib/dto-types/package.json +0 -0
  493. /package/framework/{v0.7.2 → v0.7.3}/lib/dto-types/src/main.js +0 -0
  494. /package/framework/{v0.7.2 → v0.7.3}/lib/duration/package.json +0 -0
  495. /package/framework/{v0.7.2 → v0.7.3}/lib/duration/src/main.js +0 -0
  496. /package/framework/{v0.7.2 → v0.7.3}/lib/error-ref/package.json +0 -0
  497. /package/framework/{v0.7.2 → v0.7.3}/lib/error-ref/src/main.js +0 -0
  498. /package/framework/{v0.7.2 → v0.7.3}/lib/generator/index.js +0 -0
  499. /package/framework/{v0.7.2 → v0.7.3}/lib/i18n/package.json +0 -0
  500. /package/framework/{v0.7.2 → v0.7.3}/lib/i18n/src/main.js +0 -0
  501. /package/framework/{v0.7.2 → v0.7.3}/lib/idempotency/package.json +0 -0
  502. /package/framework/{v0.7.2 → v0.7.3}/lib/idempotency/src/main.js +0 -0
  503. /package/framework/{v0.7.2 → v0.7.3}/lib/image-build/package.json +0 -0
  504. /package/framework/{v0.7.2 → v0.7.3}/lib/image-build/src/main.js +0 -0
  505. /package/framework/{v0.7.2 → v0.7.3}/lib/index.js +0 -0
  506. /package/framework/{v0.7.2 → v0.7.3}/lib/inherits/LICENSE +0 -0
  507. /package/framework/{v0.7.2 → v0.7.3}/lib/inherits/README.md +0 -0
  508. /package/framework/{v0.7.2 → v0.7.3}/lib/inherits/package.json +0 -0
  509. /package/framework/{v0.7.2 → v0.7.3}/lib/inherits/src/main.js +0 -0
  510. /package/framework/{v0.7.2 → v0.7.3}/lib/inspector-events/package.json +0 -0
  511. /package/framework/{v0.7.2 → v0.7.3}/lib/inspector-events/src/main.js +0 -0
  512. /package/framework/{v0.7.2 → v0.7.3}/lib/inspector-redact/package.json +0 -0
  513. /package/framework/{v0.7.2 → v0.7.3}/lib/inspector-redact/src/main.js +0 -0
  514. /package/framework/{v0.7.2 → v0.7.3}/lib/instrument/package.json +0 -0
  515. /package/framework/{v0.7.2 → v0.7.3}/lib/instrument/src/main.js +0 -0
  516. /package/framework/{v0.7.2 → v0.7.3}/lib/job/package.json +0 -0
  517. /package/framework/{v0.7.2 → v0.7.3}/lib/job/src/main.js +0 -0
  518. /package/framework/{v0.7.2 → v0.7.3}/lib/job-store.js +0 -0
  519. /package/framework/{v0.7.2 → v0.7.3}/lib/json-config-header/package.json +0 -0
  520. /package/framework/{v0.7.2 → v0.7.3}/lib/json-config-header/src/main.js +0 -0
  521. /package/framework/{v0.7.2 → v0.7.3}/lib/kv/package.json +0 -0
  522. /package/framework/{v0.7.2 → v0.7.3}/lib/kv/src/main.js +0 -0
  523. /package/framework/{v0.7.2 → v0.7.3}/lib/kv-store.js +0 -0
  524. /package/framework/{v0.7.2 → v0.7.3}/lib/lane/package.json +0 -0
  525. /package/framework/{v0.7.2 → v0.7.3}/lib/lane/src/main.js +0 -0
  526. /package/framework/{v0.7.2 → v0.7.3}/lib/loading-state/package.json +0 -0
  527. /package/framework/{v0.7.2 → v0.7.3}/lib/loading-state/src/main.js +0 -0
  528. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/README.md +0 -0
  529. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/package.json +0 -0
  530. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/containers/default/index.js +0 -0
  531. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/containers/file/index.js +0 -0
  532. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/containers/mq/index.js +0 -0
  533. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/containers/mq/listener.js +0 -0
  534. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/containers/mq/speaker.js +0 -0
  535. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/helper.js +0 -0
  536. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/main.js +0 -0
  537. /package/framework/{v0.7.2 → v0.7.3}/lib/logger/src/redact.js +0 -0
  538. /package/framework/{v0.7.2 → v0.7.3}/lib/maintenance/package.json +0 -0
  539. /package/framework/{v0.7.2 → v0.7.3}/lib/maintenance/src/main.js +0 -0
  540. /package/framework/{v0.7.2 → v0.7.3}/lib/math/index.js +0 -0
  541. /package/framework/{v0.7.2 → v0.7.3}/lib/mcp-dispatch/package.json +0 -0
  542. /package/framework/{v0.7.2 → v0.7.3}/lib/mcp-dispatch/src/main.js +0 -0
  543. /package/framework/{v0.7.2 → v0.7.3}/lib/mcp-http/package.json +0 -0
  544. /package/framework/{v0.7.2 → v0.7.3}/lib/mcp-http/src/main.js +0 -0
  545. /package/framework/{v0.7.2 → v0.7.3}/lib/mcp-server/package.json +0 -0
  546. /package/framework/{v0.7.2 → v0.7.3}/lib/mcp-server/src/main.js +0 -0
  547. /package/framework/{v0.7.2 → v0.7.3}/lib/merge/README.md +0 -0
  548. /package/framework/{v0.7.2 → v0.7.3}/lib/merge/package.json +0 -0
  549. /package/framework/{v0.7.2 → v0.7.3}/lib/merge/src/main.js +0 -0
  550. /package/framework/{v0.7.2 → v0.7.3}/lib/message-validator/package.json +0 -0
  551. /package/framework/{v0.7.2 → v0.7.3}/lib/message-validator/src/main.js +0 -0
  552. /package/framework/{v0.7.2 → v0.7.3}/lib/metrics/package.json +0 -0
  553. /package/framework/{v0.7.2 → v0.7.3}/lib/metrics/src/main.js +0 -0
  554. /package/framework/{v0.7.2 → v0.7.3}/lib/model.js +0 -0
  555. /package/framework/{v0.7.2 → v0.7.3}/lib/money/package.json +0 -0
  556. /package/framework/{v0.7.2 → v0.7.3}/lib/money/src/main.js +0 -0
  557. /package/framework/{v0.7.2 → v0.7.3}/lib/multipart/package.json +0 -0
  558. /package/framework/{v0.7.2 → v0.7.3}/lib/multipart/src/main.js +0 -0
  559. /package/framework/{v0.7.2 → v0.7.3}/lib/net-locality/package.json +0 -0
  560. /package/framework/{v0.7.2 → v0.7.3}/lib/net-locality/src/main.js +0 -0
  561. /package/framework/{v0.7.2 → v0.7.3}/lib/nunjucks-filters/README.md +0 -0
  562. /package/framework/{v0.7.2 → v0.7.3}/lib/nunjucks-filters/package.json +0 -0
  563. /package/framework/{v0.7.2 → v0.7.3}/lib/nunjucks-filters/src/main.js +0 -0
  564. /package/framework/{v0.7.2 → v0.7.3}/lib/nunjucks-resolver/package.json +0 -0
  565. /package/framework/{v0.7.2 → v0.7.3}/lib/nunjucks-resolver/src/main.js +0 -0
  566. /package/framework/{v0.7.2 → v0.7.3}/lib/priority/package.json +0 -0
  567. /package/framework/{v0.7.2 → v0.7.3}/lib/priority/src/main.js +0 -0
  568. /package/framework/{v0.7.2 → v0.7.3}/lib/proc.js +0 -0
  569. /package/framework/{v0.7.2 → v0.7.3}/lib/push/package.json +0 -0
  570. /package/framework/{v0.7.2 → v0.7.3}/lib/push/src/main.js +0 -0
  571. /package/framework/{v0.7.2 → v0.7.3}/lib/rate-limit/package.json +0 -0
  572. /package/framework/{v0.7.2 → v0.7.3}/lib/rate-limit/src/main.js +0 -0
  573. /package/framework/{v0.7.2 → v0.7.3}/lib/release-watch/package.json +0 -0
  574. /package/framework/{v0.7.2 → v0.7.3}/lib/release-watch/src/main.js +0 -0
  575. /package/framework/{v0.7.2 → v0.7.3}/lib/render-cache/package.json +0 -0
  576. /package/framework/{v0.7.2 → v0.7.3}/lib/render-cache/src/main.js +0 -0
  577. /package/framework/{v0.7.2 → v0.7.3}/lib/render-cache-store.js +0 -0
  578. /package/framework/{v0.7.2 → v0.7.3}/lib/routing/README.md +0 -0
  579. /package/framework/{v0.7.2 → v0.7.3}/lib/routing/build.json +0 -0
  580. /package/framework/{v0.7.2 → v0.7.3}/lib/routing/package.json +0 -0
  581. /package/framework/{v0.7.2 → v0.7.3}/lib/routing/src/main.js +0 -0
  582. /package/framework/{v0.7.2 → v0.7.3}/lib/routing-introspect/package.json +0 -0
  583. /package/framework/{v0.7.2 → v0.7.3}/lib/routing-introspect/src/main.js +0 -0
  584. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/package.json +0 -0
  585. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/backends/env.js +0 -0
  586. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/backends/exec.js +0 -0
  587. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/backends/file.js +0 -0
  588. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/declaration.js +0 -0
  589. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/env-file.js +0 -0
  590. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/main.js +0 -0
  591. /package/framework/{v0.7.2 → v0.7.3}/lib/secrets/src/sources.js +0 -0
  592. /package/framework/{v0.7.2 → v0.7.3}/lib/security-headers-emitter/package.json +0 -0
  593. /package/framework/{v0.7.2 → v0.7.3}/lib/security-headers-emitter/src/main.js +0 -0
  594. /package/framework/{v0.7.2 → v0.7.3}/lib/session-lifetime/package.json +0 -0
  595. /package/framework/{v0.7.2 → v0.7.3}/lib/session-lifetime/src/main.js +0 -0
  596. /package/framework/{v0.7.2 → v0.7.3}/lib/session-store.js +0 -0
  597. /package/framework/{v0.7.2 → v0.7.3}/lib/shell.js +0 -0
  598. /package/framework/{v0.7.2 → v0.7.3}/lib/sqlite-driver.js +0 -0
  599. /package/framework/{v0.7.2 → v0.7.3}/lib/sri/package.json +0 -0
  600. /package/framework/{v0.7.2 → v0.7.3}/lib/sri/src/main.js +0 -0
  601. /package/framework/{v0.7.2 → v0.7.3}/lib/state.js +0 -0
  602. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/package.json +0 -0
  603. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/local-cas.js +0 -0
  604. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/local-stream.js +0 -0
  605. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/local.js +0 -0
  606. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/main.js +0 -0
  607. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/meta-store.js +0 -0
  608. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/s3.js +0 -0
  609. /package/framework/{v0.7.2 → v0.7.3}/lib/storage/src/util.js +0 -0
  610. /package/framework/{v0.7.2 → v0.7.3}/lib/storage-store.js +0 -0
  611. /package/framework/{v0.7.2 → v0.7.3}/lib/swig-filters/README.md +0 -0
  612. /package/framework/{v0.7.2 → v0.7.3}/lib/swig-filters/package.json +0 -0
  613. /package/framework/{v0.7.2 → v0.7.3}/lib/swig-filters/src/main.js +0 -0
  614. /package/framework/{v0.7.2 → v0.7.3}/lib/swig-resolver/package.json +0 -0
  615. /package/framework/{v0.7.2 → v0.7.3}/lib/swig-resolver/src/main.js +0 -0
  616. /package/framework/{v0.7.2 → v0.7.3}/lib/template-loaders/package.json +0 -0
  617. /package/framework/{v0.7.2 → v0.7.3}/lib/template-loaders/src/loaders/http.js +0 -0
  618. /package/framework/{v0.7.2 → v0.7.3}/lib/template-loaders/src/loaders/memory.js +0 -0
  619. /package/framework/{v0.7.2 → v0.7.3}/lib/template-loaders/src/main.js +0 -0
  620. /package/framework/{v0.7.2 → v0.7.3}/lib/url/README.md +0 -0
  621. /package/framework/{v0.7.2 → v0.7.3}/lib/url/index.js +0 -0
  622. /package/framework/{v0.7.2 → v0.7.3}/lib/url/routing.json +0 -0
  623. /package/framework/{v0.7.2 → v0.7.3}/lib/uuid/package.json +0 -0
  624. /package/framework/{v0.7.2 → v0.7.3}/lib/uuid/src/main.js +0 -0
  625. /package/framework/{v0.7.2 → v0.7.3}/lib/validator.js +0 -0
  626. /package/framework/{v0.7.2 → v0.7.3}/lib/watcher/package.json +0 -0
  627. /package/framework/{v0.7.2 → v0.7.3}/lib/watcher/src/main.js +0 -0
  628. /package/framework/{v0.7.2 → v0.7.3}/lib/ws-framing/package.json +0 -0
  629. /package/framework/{v0.7.2 → v0.7.3}/lib/ws-framing/src/main.js +0 -0
  630. /package/framework/{v0.7.2 → v0.7.3}/lib/ws-query/package.json +0 -0
  631. /package/framework/{v0.7.2 → v0.7.3}/lib/ws-query/src/main.js +0 -0
  632. /package/framework/{v0.7.2 → v0.7.3}/lib/ws-session/package.json +0 -0
  633. /package/framework/{v0.7.2 → v0.7.3}/lib/ws-session/src/main.js +0 -0
package/llms.txt CHANGED
@@ -137,7 +137,7 @@ module.exports = ApiContentController;
137
137
  - Always null `local.req/res/next` at response exit (done automatically in render path).
138
138
  - `createTestInstance(deps)` creates an isolated instance for unit tests without touching production state.
139
139
  - `async` controller actions are fully supported — the router attaches `.catch()` to any thenable returned by an action and routes rejections to `throwError(response, 500, ...)`; `self.query()` owns an `async` callback's rejection the same way at every delivery seam (callback form and `{onComplete}` facade, both transports — a rejected callback promise answers 500 instead of hanging; #B399). **The fluent handle is a PER-CALL channel since #B475 (measured 2026-09-05):** before it, a second `self.query(...).onComplete()` on the same controller evicted the first listener (`removeAllListeners('query#complete')` + `once` on the shared instance emitter), so the first callback never fired and the survivor could receive the other call's payload — both transports, no log line — and nine synchronous failure paths (missing host, open circuit, unreadable CA, the outer catch) returned `emit()`'s boolean instead of the handle, so `.onComplete` threw `TypeError` at the call site. Now `query()` mints one channel per call whenever no callback (or `null`) is given: the handle comes back on EVERY path, chains (`onComplete()` returns it), fires every registration, delivers a registration made after settlement on the next tick, hands a synchronous failure to `cb(err)` on the next tick, and rides the callback path so every delivery guard and every retry carries it; fluent queries also gain #MS5 circuit outcome recording. `query#complete` is emitted on the controller only when nothing consumed the outcome (no callback, no `onComplete`), one tick after settlement. Same fix for `self.store(target).onComplete(cb)` (see #210). **#B479 closed the tenth path (2026-09-05):** the nested-render guard at the top of `query()` (`renderingStack` deeper than one frame — the state a `requireController`'d controller, which shares the caller's options object, produces once a second render is entered) returned the literal `false` and delivered nothing on either form; it now refuses through the same per-call channel with an `Error` whose `code` is `NESTED_RENDER` — in-line for the callback form, next tick through the handle — and still never contacts the upstream. Use `await entity.method()` directly. For a Shell command use `await onCompleteCall(new lib.Shell().run(cmd, true))` or `await onCompleteCall(run([cmd, arg], { cwd }))`; PathObject ops (`mkdir`/`cp`/`mv`/`rm`) are Node-style callback methods that return `undefined`, so `onCompleteCall` cannot wrap them — promisify their callback instead, and note `_(p)` returns a normalised STRING while only `new _(p)` returns the PathObject that carries those methods.
140
- - `self.setEarlyHints(links)` — send a 103 Early Hints informational response. Call before the terminal method. `links` is a string or array of `Link` header values. HTTP/2: `stream.additionalHeaders({ ':status': 103 })`; HTTP/1.1: `res.writeEarlyHints()` (Node.js 18.11+). Silent no-op when unsupported. Returns `self`. **Also automatic** (WORKING since 0.6.28 — #B496; in every earlier release the auto path was DEAD, the read running before the only writer, so the hint was always skipped): `render()` auto-sends 103 for the view's declared CSS/JS preloads before template compilation in HTTP/2 production mode, zero config. It is computed after the #SPA1 negotiation block (which resolves whether the request is a fragment — the delegates filter assets to common-only for those) and before the delegate dispatch, resolving its view config as `errOptions || local.options` like all five delegates. The router's `h2Links` seed is restored immediately after the hint, so the delegate rebuilds the final-200 `Link` header byte-identically (measured live). No auto hint for XHR requests, in dev, or for SRI'd assets (#OW3 — a hint carries no integrity metadata). Only DECLARED assets can be hinted; `getAssets()`-parsed images/fonts reach the browser via the 200 header, since the template has not compiled at hint time. A custom-error render re-seeds `h2Links` before its own `render()` (#B497, 0.6.28), so its 103 and its 200 describe the error page. On a route whose template fails to compile, two 103s go out — one per render — measured identically before and after #B497.
140
+ - `self.setEarlyHints(links)` — send a 103 Early Hints informational response. Call before the terminal method. `links` is a string or array of `Link` header values. HTTP/2: `stream.additionalHeaders({ ':status': 103, link })`, entries joined with `', '`, not capped. HTTP/1.1: since 0.7.3 (#B771) ONLY when `settings.json > server.earlyHintsOverHTTP1 === true` (strictly the boolean; default off — browsers act on a 103 only over HTTP/2 and HTTP/3, and nginx < 1.29, e.g. the Ubuntu 22.04/24.04 and Debian 12 packages, takes an upstream 103 for the FINAL response, so every HTML page behind it broke: Chromium `ERR_HTTP2_PROTOCOL_ERROR`); then `res.writeEarlyHints({ link: [entries] })`, one array element per entry (#B770: node checks a string as ONE link value, a `', '`-joined list failed that check and was silently dropped, so a multi-entry explicit hint never went out over HTTP/1.1), capped at the page's `preloadHintsMaxSize`; node rejects an entry whose parameter holds a space (an `imagesrcset`) and the whole 103 is dropped. Silent no-op when unsupported or on any error. Returns `self`. **Also automatic** (WORKING since 0.6.28 — #B496; in every earlier release the auto path was DEAD, the read running before the only writer, so the hint was always skipped): `render()` auto-sends 103 for the view's declared CSS/JS preloads before template compilation in HTTP/2 production mode, zero config. It is computed after the #SPA1 negotiation block (which resolves whether the request is a fragment — the delegates filter assets to common-only for those) and before the delegate dispatch, resolving its view config as `errOptions || local.options` like all five delegates. The router's `h2Links` seed is restored immediately after the hint, so the delegate rebuilds the final-200 `Link` header byte-identically (measured live). **Size and switch (0.7.3, #B765):** the auto 103 AND render-swig's 200 `link` header (the only delegate that sends one; swig-async and both nunjucks delegates send the 103 alone) go through `core/controller/preload-hints.js` `shapeLinks()`: exact-URL dedup, first wins (#B767), then a cap at an entry boundary at `templates.json > preloadHintsMaxSize` bytes (default 1024 in the framework `_common`; `0` = no cap; a negative or non-integer value falls back to 1024 with ONE warning per process), order kept (declared CSS, declared JS, then layout assets); `preloadHintsEnabled: false` sends neither (explicit `setEarlyHints()` unaffected). render-swig memoises the SHAPED value on the compiled-template cache entry, so cache hits replay it. Both keys work per page: `config.js`'s page loop keeps a page's own boolean (#B772 — it combined page and `_common` through `lib/merge`, and `merge(false, true)` returns `true`, so a page's `false` over a `_common` `true` was lost). Layout `<link rel="stylesheet">` / `<script src>` get `as=style` / `as=script` since 0.7.3 (#B766 — a `switch (type)` compared the rel string with booleans, so no case matched) unless the tag carries `integrity`, `crossorigin`, `nomodule` or `type=module`, a `media` other than all/screen, or an `alternate` rel. **The layout scan (`getAssets()`, `core/server.js`) reads ONE START TAG per match**, ended by its first `>` outside a quoted attribute value (`media="(width > 600px)"`) — until 0.7.3 a match ran to the end of its SOURCE LINE and kept the line's last `src`/`href`, so tags sharing a line were one entry: a script hinted `as=image`, a one-line `<picture>`'s `<img>` lost, a minified layout unhinted (#B768). A `<script>` element is consumed whole (its inline text is never markup), a tag's kind comes from its own name, `</picture>` ends the srcset its `<source>` tags collected (attached to an `<img>` only), and nothing carries from one tag to the next (#B775: a srcset-only `<img>` first in a layout threw, 500). A URL written with the webroot resolves: the public webroot (`page.environment.webroot`, proxy prefix included), else `server.webroot`, is stripped once before `getAssetFilenameFromUrl()`, the entry keeping the URL as written (#B769). A miss is the resolver's exact `'404.html'` (#B777: `/404/` anywhere in the path left a bundle under a directory holding `404` with no layout hint). The CSS `url()` scan of the layout's stylesheets (Inspector data, never hinted) skips a `url()` it cannot read (`url(#id)`, a fragment, no dot: each a 500 before, #B774) and never replaces a layout entry (#B776). render-swig places gina's scripts from the page's own `javascriptsDeferEnabled`, the value `getNodeRes()` writes `defer` from (#B778: a `_common` fallback put a page's `false` in `<head>` without `defer` once #B772 let it through; fixed before release). `ginaEnabled` was never read and is no longer in the framework `_common` (#B773). No auto hint for XHR requests, in dev, or for SRI'd assets (#OW3 — a hint carries no integrity metadata). Only DECLARED assets can be in the 103; `getAssets()`-parsed layout assets reach the browser via the 200 header, since the template has not compiled at hint time. A custom-error render re-seeds `h2Links` before its own `render()` (#B497, 0.6.28), so its 103 and its 200 describe the error page. On a route whose template fails to compile, two 103s go out — one per render — measured identically before and after #B497.
141
141
  - `self.renderStream(asyncIterable, contentType)` — stream an `AsyncIterable` as a chunked HTTP response without buffering. `contentType` defaults to `text/event-stream` (SSE). Each yielded string/Buffer becomes `data: {chunk}\n\n` for SSE; raw for other content types. HTTP/2: `stream.respond()` + `stream.write()` + `stream.end()`. HTTP/1.1: automatic chunked transfer-encoding. `x-accel-buffering: no` set automatically for SSE. Fire-and-forget — do not `await`. Required for LLM token streaming via `ai.client` with `stream: true`. **Honours a caller-set `response.statusCode` on BOTH engines (#B351, 0.6.8)** — the h2 frame carries `response.statusCode || 200` and the h1 arm assigns `response.statusCode || 200`, so a controller may pre-set 206/416/404 before calling. Until 0.6.8 both arms were 200-only (the h2 frame hardcoded the pseudo-header literal, the h1 arm clobbered a pre-set code), which made `206 Partial Content` unreachable through renderStream — the reason it blocked Range serving. Same class as #B172: **a hand-built h2 frame must never carry a literal `':status'`**, because `setHeader(':status', …)` throws and no later header merge can repair a pseudo-header; the pending-header merge deliberately does not overwrite `:status`.
142
142
 
143
143
  ```javascript
@@ -853,7 +853,7 @@ Dev-mode query instrumentation captures every database query tied to the current
853
853
  - **Two-guard `@<project>` requirement asymmetry in `lib/cmd/helper.js`** — a `project:*` command that should run without `@<project>` must be exempted in BOTH guards: the early task-shape guard (`!/^project\:(list|help|status)/`) AND the later projectName-resolution guard (`!/\:list$/.test(cmd.task)` → widened to also allow `^project:status$`). Exempting only the first leaves the second falling through to cwd-project-inference and a "No project name found" error before the handler runs. `project:list` is the working precedent that null `projectName` survives `loadAssets()`.
854
854
  - **Single-bundle CLI handlers read `cmd.name` / `self.name`, NOT `self.bundle`** — CmdHelper sets `cmd.name = cmd.bundles[0]` only when exactly one bundle positional is present (`lib/cmd/helper.js`, the `cmd.bundles.length == 1` branch); `cmd.bundles` is the array for bulk operations. There is no `self.bundle` property. Precedent: `bundle:stop` / `bundle:start` / `bundle:status` all read `self.name`. (Worked example: a sub-agent investigation reported the slot as `self.bundle`; a single read of `helper.js` refuted it before the handler shipped — verify agent-relayed property names against source.)
855
855
  - **`gina start` does not need sudo with a user-prefix install** — `npm install -g --prefix ~/.npm-global` (standard Gina setup) runs as the current user and doesn't touch `/var/run/`. The "Needs to be launched as [sudo]" comment in `lib/cmd/framework/start.js` only applies to system-wide installs.
856
- - **`gina start` exits with code 1 even on success** — the startup script detaches the daemon; the shell child exits 1 as part of detachment. Always use `gina status` to confirm rather than the exit code. Sibling: a `gina start` framework server left running can hang the full test suite AND bundle boot — the hang is the MQ log-listener connect left pending forever against a degraded daemon; a daemon on a non-canonical port traces to a corrupted persisted `port` in `~/.gina/<shortVersion>/settings.json`, not to `~/.gina/procs.json`, which no boot path reads (its only consumers are `gina stop`'s kill targeting and an address-in-use diagnostic). #B180 (fixed): `removeRunningProc()` deleted the registry entry under the constructing bundle's key instead of the key matched by pid, so dead `gina-*` records were rewritten back instead of removed (and a live record could be destroyed when the keys differed) — a `procs.json` written by an older framework may still carry stale dead entries; with the daemon stopped, clearing the file once is safe.
856
+ - **`gina start`'s exit code says whether the framework started (#B759 + #B760 + #B761, fixed in 0.7.3)** — `bin/gina` runs `start` / `framework:start` through `runAsSubProcess()`, which spawns `bin/cli` as a detached daemon child and exits 0 once the child prints « Framework ready for connections » or « Framework already running ». When the child ends before either line, the wrapper waits until its output is drained (`close`) and exits with the child's code: 1 when that code is 0, 128 + the signal number when a signal ended it (137 for SIGKILL). A stderr warning is printed and no longer ends the start; Debugger lines and « address already in use » still exit 1. Before 0.7.3 a start that failed early (a refused `GINA_VERSION`, a crash during boot) returned 0, and any stderr chunk matching `warning` (case-insensitive) made the wrapper exit 1 at once, after which the orphaned daemon died on its next write (EPIPE), so the start was aborted; the advice that stood here, « exits with code 1 even on success », does not reproduce (a real start returns 0). When the framework port is already in use (#B761), `bin/cmd` prints « Framework already running … [ <pid> ] » (the wrapper's exit 0) only when a pid `~/.gina/procs.json` records for the port is alive and a framework daemon, read per pid by `lib/cmd/framework/inc/ps-titles.js` `readPidTitle()` (`ps -ww -p <pid> -o stat=,command=`; `-ww` because procps-ng cuts the command to `$COLUMNS` even when piped): titled `gina-v<version>` on Node, or, on Bun, which does not show `process.title` to `ps`, still running `…/bin/cli start`; where `ps` cannot tell (Windows, busybox), the alive check decides. Otherwise `inc/listen-error.js` sends the cause, the fix (`gina framework:set --port=<port>`) and, outside Windows, an `lsof` finder to stderr and the start exits 1, which frees the MQ port; any other listen error (EACCES) exits 1 instead of hanging, and an error once the socket listens (an accept failure such as EMFILE) is only logged. Before 0.7.3 any entry for the port was trusted unchecked: a foreign listener read « already running » (« PID `null` »), a stale entry named a dead pid, and both exited 0 with a half-started daemon on the MQ port. Sibling: a `gina start` framework server left running can hang the full test suite AND bundle boot — the hang is the MQ log-listener connect left pending forever against a degraded daemon; a daemon on a non-canonical port traces to a corrupted persisted `port` in `~/.gina/<shortVersion>/settings.json`, not to `~/.gina/procs.json`, which no boot path reads (its only consumers are `gina stop`'s kill targeting and `gina start`'s held-port check). #B180 (fixed): `removeRunningProc()` deleted the registry entry under the constructing bundle's key instead of the key matched by pid, so dead `gina-*` records were rewritten back instead of removed (and a live record could be destroyed when the keys differed) — a `procs.json` written by an older framework may still carry stale dead entries; with the daemon stopped, clearing the file once is safe.
857
857
  - **CLI reserved flags — `--port` and `--version` must be renamed in subcommands** — both consumed by the framework's global CLI parser (`utils/helper.js::filterArgs` + `bin/cli:301`) before the subcommand sees argv. Use domain-prefixed forms: `--connector-port=`, `--driver-version=`. Un-prefixed forms are silently swallowed.
858
858
  - **CLI scope grammar — positional-absence, not flags** — for commands that apply to either a single bundle or the whole project, use `[<bundle>] @<project>` and signal project-wide by omitting `<bundle>`. Never reintroduce `--scope=bundle` (`scope` is reserved for data isolation: local/beta/production/testing) or `--shared`.
859
859
  - **Scope and environment names are checked WHOLE, by two dependency-free rule modules** — `lib/cmd/scope/inc/name.js` (`isValidScopeName`, #B626) and `lib/cmd/env/inc/name.js` (`isValidEnvName`, #B639): letters, digits, `_`, `.`, `-`, the first character a lowercase letter, a digit, `_` or `.`; never `.` / `..`, and never the name of a property every object inherits (`__proto__`, `constructor`, gina's own `count` …) — the project files are indexed by these names, and an existence test such as `typeof(obj[name]) == 'undefined'` would reach the shared prototype. Environments also refuse `global`, the overlay token of `<name>.global.json`. `scope:add` / `env:add` refuse a bad name with the module's `describeInvalid…` message and write nothing; `project:add` applies both rules to `--scope=` / `--env=` before anything is written (#B640), while `project:import` skips the check — the CLI bootstrap (`framework/init.js` checkScope / checkEnv) already refuses any value the registered project does not list, and a name an older release registered must keep importing; the slot commands `scope:link-local` / `scope:link-production` / `env:link-dev` set the project's `local_scope` / `production_scope` / `dev_env` in `projects.json` (moving `def_scope` / `def_env` along when it held the old slot) and create no symlink; they check the project before looking the value up in it (#B641); they used to test the first character only, so the retired `<bundle>/<scope>` and `<bundle>/<env>` forms registered verbatim as project-wide names and `Staging` was dropped without a word. The modules live in `inc/` so the command loader never takes them for `<group>:<action>` handlers. Known residual: the shared CLI helper compiles `new RegExp(cmd.name + …)` from the raw token before any handler check, so a token with an unbalanced `(` still exits through `console.emerg` rather than the clean message. `project:add` / `project:import` read `--scope=` / `--env=` / `--path=` WHOLE — each value is everything after the FIRST `=`, and a flag counts only where its name starts the argument (#B644: `--scope=a=b` used to register `a`, and a `--path` holding `--scope=` was read as that flag); neither flag changes the project's `def_scope` / `def_env` — `scope:use` / `env:use` do. `env:add <env> @<project>` gives every bundle of the project its own ports for the new env, writes its `env.json` blocks and exits (#B643 — it used to list the env on the project only, silently); the global `env:add <env>` registers the name for every project WITHOUT ports (#B652, open), and a later `env:add <env> @<project>` gives them, leaving the other envs' ports unchanged.
@@ -913,7 +913,7 @@ Dev-mode query instrumentation captures every database query tied to the current
913
913
  - **A cached route entry is a CANDIDATE, not a verdict — the warm path must honor `.past` (#B422).** The route cache is keyed `req.method+':'+pathname` and on the isaac engine `request.url` is query-stripped before dispatch, so ALL query variants of one pathname share ONE cache entry. `getCached()` re-runs `compareUrls()` on every hit — requirements ARE re-evaluated — but returns the `foundRoute` object UNCONDITIONALLY with the verdict in `.past` (`request.routing` is populated only when `.past` is true). The dispatch call site used to test the object's truthiness, force-matching a requirements-REJECTED request: dispatch proceeded with `req.routing` unset, the header-composition stub reached `router.route()`, and the `params.param.control` deref threw inside the async chain — unhandled promise rejection, connection DROPPED with no HTTP response at all. Whichever variant of a pathname was seen first decided what was cached (request-ORDER dependence: the same URL answers differently depending on which variant warmed the entry). Fixed: the warm path treats any outcome other than `.past === true` — requirements rejected, or a throwing `validator::` requirement — as a cache MISS: it restores the request bags the failed `compareUrls` run contaminated (`req.params` + `req[method]`, the same surface the cold loop resets per iteration) from a pre-`getCached` capture and falls through to the cold scan, which owns every terminal (404 when nothing matches, the sibling rule when one does, the cold catch's 500 naming the rule on a throw). The cache entry is NOT invalidated — variants the requirements accept stay warm; a pathname whose variants match DIFFERENT rules serves the second rule correctly but never caches it (first-cached wins the key — `cache()` only writes absent keys). Third instance of the un-checked-`.past` class (two prior fixes in `lib/routing` carry `was: if (isRoute.past)` comments); the other three `compareUrls` call sites all check `.past`. Defence in depth: `router.route()` now answers a named 500 instead of throwing when dispatched without a resolved `params.param.control`. Server-side only (`lib/routing` untouched; `core/server.js` + `core/router.js` are not browser-bundled) — pickup is a bundle restart, no re-bake. Tests: `test/core/server-route-cache-verdict.test.js` (source pins + the real-module `cache()`→`getCached()` verdict behaviorals).
914
914
  - **A literal `404:[<METHOD>]<rule>@<bundle>` in a rendered href means the template's url helper could not resolve the rule** — the server logs a `Routing Exception` naming the rule at render time (the only emitters are the template url filters' catch), and the browser console warns `route [ %r ] is called but not found inside your view` on the FIRST client-side sighting of each missing route — repeats are counted, not re-warned, on the `gina.notFound` registry (`{count, message}` per route key). Typical causes, most likely first: a mistyped rule or bundle name at the call site; a boot-degraded routing table (a sibling bundle's config/routing.json missing at boot — fail-fast since 0.5.21, the not-found message appends the bundle's rule count so degraded-vs-typo is tellable); a routing table not yet reloaded after a deploy.
915
915
  - **Which method a rule serves a request under (0.7.0 — #B662 #B660 #B659 #B667 #B675).** `_handleDispatch` skips a single-method rule of another method before comparing URLs, so a wrong method on a URL whose rules each declare one method answers **404**, identical to an unknown URL; a **405** comes only from a multi-method rule (`"GET,POST"`) whose URL matched, and it carries the RFC 9110 `Allow` field (`buildAllowHeader`: the refusing rules' methods in declaration order, `HEAD` after every `GET`). **HEAD** is matched as `HEAD` on every rule that serves `GET`, a `:param` URL and a method list included — `lib/routing`'s `fitsWithRequirements` refuses a `:param` match whose rule method differs from the request's, so matching the rule as `GET` answered `HEAD /items/:id` with 404. A HEAD runs the GET action in full, so its answer carries a GET's headers, and once a rule has matched `_handleDispatch` sets `req.get` to `req.head` — the same object, holding the URL and query params a GET gets. processRequestData keeps a HEAD's params in `req.head` and clears `req.get`, so before this a GET action reading `req.get.<param>` unguarded answered 500 on HEAD (#B675; static URLs since 0.3.0). `req.method` stays `HEAD`, which is what keeps the body off the wire. **The GET→DELETE override** — a `GET` served by a rule declaring `DELETE`, which the popin and link plugins' anchors rely on — is granted only when `isGetToDeleteOverrideAllowed(req)`: `req.isXMLRequest === true` AND `!lib.admin.isCrossOriginWrite(req)` (`Sec-Fetch-Site` same-origin/none pass, same-site/cross-site refused, a foreign `Origin` refused, no browser signal passes). Any other `GET` gets the single-method 404, or the 405 on a method list containing `DELETE`; the override sets the literal `DELETE`, never the rule's list. Why both conditions: granted to any `GET`, the rewrite let a cross-site navigation carrying a `SameSite=Lax` session cookie run a DELETE action, and on the Express engine it bypassed the Csrf plugin, which `app.use` mounts BEFORE dispatch so it saw a safe `GET`; the XHR header alone is not enough, because a bundle whose `access-control-allow-origin` is empty reflects the request origin with credentials in a preflight. The override reaches a static URL, or a `:param` URL whose key carries a requirement (the requirements branch of `parseRouting` scores it); a requirement-less `:param` DELETE rule never matched a `GET`. On isaac the Csrf plugin runs AFTER the rewrite, so an overridden request needs a CSRF token there.
916
- 142. **`bin/cli` resolves the framework dir from `GINA_VERSION` (env/persisted) falling back to `package.json` version, then `require()`s `framework/v<version>/lib/generator` — now guarded by `fs.existsSync(frameworkPath)` BEFORE that require.** A `GINA_VERSION` pointing at a non-installed version (a stale pin, or a bind-mounted dev tree at a different version) otherwise throws `MODULE_NOT_FOUND` that the surrounding `try/catch` mislabels as `gina: could not load [ package.json ]` (package.json actually loaded fine), and the legitimate `existsSync` guard sits AFTER the require so it never fires — every CLI command then fails opaquely (in a container, `project:import` silently fails, so bundles report "not registered" on start). The guard now fails fast with a clear `gina framework:add <version>` message via `process.stderr.write` (`console` is not reassigned to the framework logger until later, so `console.alert` is undefined there). Diagnostic tell: `could not load package.json` followed by `Cannot find module '…/framework/v<X>/lib/generator'` means framework version `<X>` is not installed, not that package.json is broken. (`8fa9c278`, 2026-05-30) **Sibling (2026-07-03) — same guard-before-dynamic-require rule, applied to command DISPATCH.** `lib/cmd/framework/init.js` `run()` — the single chokepoint that `require()`s `/cmd/<topic>/<action>.js` for BOTH offline framework AND online bundle commands — now `fs.existsSync`-guards the handler path BEFORE requiring it. A known group (listed in `bin/cli`'s `allowedOffline`) with an UNKNOWN ACTION previously threw `Cannot find module .../cmd/<group>/<action>.js`, dumped as a raw stack by `run()`'s catch: `gina framework:connector` (connector is its OWN top-level group), `gina connector` / `gina connector --help` (auto-prefixed to the same `framework:connector` by `bin/cli:373`), and `gina -V` (→ `framework:-V`; only lowercase `-v` and `--version` are aliased) all hit it. The guard now prints a clean `'<group>:<action>' is not a valid command.` + a `gina help` / `gina help <group>` pointer, plus a did-you-mean when the action names a real command group (gated on the SAME `/^[a-z][a-z0-9-]*$/` + `<action>/help.txt`-exists test `gina help <group>` uses), exits 1, and mirrors the message to `opt.client` on the socket path; the try/catch still keeps the stack for errors thrown INSIDE a resolved handler (real crash vs. typo cleanly split by the `existsSync` check). `bin/cli:426-432` already rejected an unknown GROUP cleanly — this closes the known-group/unknown-action gap. Rule: `fs.existsSync`-guard every dynamic handler require and emit a clean unknown-command message; reserve the stack for errors from inside a resolved handler. Tests `test/lib/cmd-unknown-command.test.js`; server-side (no dist rebuild).
916
+ 142. **`bin/cli` resolves the framework dir from `GINA_VERSION` (env/persisted) falling back to `package.json` version, then `require()`s `framework/v<version>/lib/generator` — now guarded by `fs.existsSync(frameworkPath)` BEFORE that require.** A `GINA_VERSION` pointing at a non-installed version (a stale pin, or a bind-mounted dev tree at a different version) otherwise throws `MODULE_NOT_FOUND` that the surrounding `try/catch` mislabels as `gina: could not load [ package.json ]` (package.json actually loaded fine), and the legitimate `existsSync` guard sits AFTER the require so it never fires — every CLI command then fails opaquely (in a container, `project:import` silently fails, so bundles report "not registered" on start). The guard now fails fast with a clear `gina framework:add <version>` message via `process.stderr.write` (`console` is not reassigned to the framework logger until later, so `console.alert` is undefined there). Diagnostic tell: `could not load package.json` followed by `Cannot find module '…/framework/v<X>/lib/generator'` means framework version `<X>` is not installed, not that package.json is broken. (`8fa9c278`, 2026-05-30) **Sibling (2026-07-03) — same guard-before-dynamic-require rule, applied to command DISPATCH.** `lib/cmd/framework/init.js` `run()` — the single chokepoint that `require()`s `/cmd/<topic>/<action>.js` for BOTH offline framework AND online bundle commands — now `fs.existsSync`-guards the handler path BEFORE requiring it. A known group (listed in `bin/cli`'s `allowedOffline`) with an UNKNOWN ACTION previously threw `Cannot find module .../cmd/<group>/<action>.js`, dumped as a raw stack by `run()`'s catch: `gina framework:connector` (connector is its OWN top-level group), `gina connector` / `gina connector --help` (auto-prefixed to the same `framework:connector` by `bin/cli:373`), and `gina -V` (→ `framework:-V`; only lowercase `-v` and `--version` are aliased) all hit it. The guard now prints a clean `'<group>:<action>' is not a valid command.` + a `gina help` / `gina help <group>` pointer, plus a did-you-mean when the action names a real command group (gated on the SAME `/^[a-z][a-z0-9-]*$/` + `<action>/help.txt`-exists test `gina help <group>` uses), exits 1, and mirrors the message to `opt.client` on the socket path; the try/catch still keeps the stack for errors thrown INSIDE a resolved handler (real crash vs. typo cleanly split by the `existsSync` check). `bin/cli:426-432` already rejected an unknown GROUP cleanly — this closes the known-group/unknown-action gap. Rule: `fs.existsSync`-guard every dynamic handler require and emit a clean unknown-command message; reserve the stack for errors from inside a resolved handler. Tests `test/lib/cmd-unknown-command.test.js`; server-side (no dist rebuild). **Sibling (2026-10-03, #B584) — the same guard, extended to the two ways `GINA_VERSION` arrives AFTER it.** The early guard only sees a version already in the framework environment: an exported `GINA_VERSION` is imported later (`importEnvVars(true)`) and `--version=<v>` is promoted by `filterArgs()`, so a value naming no installed framework (`latest`, an uninstalled number) passed it, and the framework init then migrated `~/.gina` to that phantom version (a `latest` key in every per-version dict of `main.json` and `gina.db`, an empty `<home>/latest/`, `Gina I/O vlatest`) while the command carried on. `bin/cli` now checks right after `filterArgs()`: a `GINA_VERSION` other than the package's own whose `<gina>/framework/v<version>` does not exist exits 1 before the framework init runs (the home is not migrated), with a synchronous `fs.writeSync(2, …)` message and, only for a value `framework:add` accepts, an `env -u GINA_VERSION gina framework:add <v>` hint (the plain hint would itself be refused while the variable is exported). A side-by-side version (`framework:add` links it under `<gina>/framework/v<v>`) is accepted. Twin fix in `filterArgs()`: its first-hyphen → `_` rewrite is meant for the flag NAME but ran on the whole argument, so a hyphen-free name lost a hyphen from its VALUE (`--version=0.7.2-alpha.2` reached the CLI as `0.7.2_alpha.2`); the value is now the argument's text after its first `=`, as given. Without that half, the refusal would refuse every installed pre-release passed with `--version=` (measured). `bin/gina-container` needs no guard: it never imports the shell environment into `process.gina` (measured: an exported `GINA_VERSION=latest` leaves `main.json` and `gina.db` byte-identical). Keys an earlier run wrote are not pruned: the registry legitimately lists hundreds of registered-but-not-installed versions, so « not installed » is no safe prune rule. Tests `test/bin/gina-version-refusal-b584.test.js`; CLI only (no dist rebuild).
917
917
 
918
918
  151. **`templates.json` pre-process pass in `core/config.js` (right after the `hasViews` line, BEFORE the routing↔template GET auto-vivify) expands two additive section-key shapes once per bundle — gina-io/gina#8 comma-separated keys + #10 `_common.config`.** #8: a comma-separated section key (`"a, b": {…}`) is split on `/\s*,\s*/` (each name `.trim()`med, empty segments skipped) and the block is replicated under each named section, MERGING into any section that already exists so a section's own keys win — `merge(existing, JSON.clone(block))`, and `lib/merge` keeps its FIRST argument on a leaf collision (`override=false` default). #10: an optional `_common.config` block is `merge(_common, _common.config)`-flattened back into `_common` then deleted, so the existing `_common.*` read sites are unchanged and a direct `_common.X` overrides `_common.config.X`. **Placement is load-bearing:** the pass must run before the GET auto-vivify `files['templates'][rule.toLowerCase()] = {}` (which keys off CLEAN route names from routing.json) — otherwise a comma key leaves the real route names "missing" → empty `{}` sections get minted AND the comma key survives to produce a dead `"a, b@bundle"` route. **Both are no-ops when absent** (no comma → single-element split → identical; no `_common.config` → untouched), so existing bundles are byte-identical — verified zero comma keys across known consumer + gina fixture `templates.json` before shipping. **#7 (a Swig-like `{ "inherit": … }` directive) was closed un-shipped**, so #8 is NOT redundant: `_common` shares to ALL routes, #8 shares a chosen SUBSET — a gap nothing else fills. Collect-then-mutate (gather comma keys first) avoids changing the object mid-`for…in`. Tests: `test/core/config-templates-preprocess.test.js` (source pins incl. placement-before-auto-vivify + a real-`merge` pure-logic replica: split / union-merge / own-keys-win / trim / empty-segment / flatten / no-op). Established 2026-06-05.
919
919
 
@@ -937,13 +937,13 @@ Dev-mode query instrumentation captures every database query tied to the current
937
937
 
938
938
  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. ⚠️ **The FIELD keeps the `Date`; the PAYLOAD takes a `yyyy-mm-dd` string, always (#B558, 0.6.32-alpha.2).** Every validator normalises the payload to the validated canonical form (`isEmail` lowercases, `isBoolean` → boolean, `isNumber` → `Number`), so `isDate` normalising is consistent — but `Date` is the one normalised type that does NOT round-trip through JSON: `JSON.stringify` renders it via `toISOString()`, a UTC **instant**, and local midnight at any positive UTC offset is the PREVIOUS day in UTC. So a browser in Paris submitting `2026-09-18` sent `2026-09-17T22:00:00.000Z` and every reader that sliced the date part stored the 17th — silent (the instant is well-formed) and invisible to a server or CI running in UTC. The written shape is mask-INDEPENDENT: the mask governs INPUT parsing, while both `requirementToSchema()` and `dto.date()` already published the field as `{type:'string', format:'date'}` (RFC 3339 full-date, explicitly NOT `date-time`), so the payload now matches the schema gina was already advertising. Consumer-visible: `req.body.<field>` is a string, not a `Date`, and `new Date('2026-09-18')` on a date-only string is UTC midnight, not local midnight. 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 splice itself was UNESCAPED until #B600-#B602 (gh#77, 0.6.33):** `getDynamisedRules` pastes every referenced value into the STRINGIFIED rule set and parses it back, so a `"`, `\` or control character in a referenced value (a textarea, a password) threw out of the whole pass (client: dead submit; server: the error escaped the plugin), at FOUR sites - `getCastedValue`'s string return AND its number-rule branch, the loop-1 default for a referenced field with no rule, the loop-2 DOM default - each through a STRING replacement that also expanded `$&`/`$'`/`$$`. Every splice is now `quoteForDynamisedRules(v)`, i.e. an escaped quote + `escapeForJsonString(escapeForJsonString(v))` + an escaped quote (the escaper touches ONLY `"`, `\` and U+0000-U+001F, so the splice is byte-identical to the old one for every value that did not throw), through function replacers, with the closing parse guarded (fallback: the rules as declared, which `is()` resolves or fails closed). A number-ruled referenced value splices raw only when it IS a number (typed, or text matching `/^ *-?\d+(?:\.\d+)? *$/` - the padding keeps `" 12"` vs `12` valid as before); other text compares as a string. Note the engine's `isNumber` is LENIENT (`parseInt`: `1"2` and `12abc` pass as the leading number), so a number field's own verdict never discriminates such values - compare `1"2` vs `1"3` instead. `is()`: the string operand is `"(?:[^"\\]|\\.)*"`, decoded with `JSON.parse` (an AUTHORED literal that is not valid JSON, `"C:\dir"`, is read verbatim as before; one that cannot be read at all fails the field, never throws); its own `$` substitution splices `JSON.stringify(value)` through a function replacer; the `(`/`)`/`return` strip runs OUTSIDE string literals only (#B601 - it made `ab(cd` equal `ab)cd`, also on route requirements); and a condition no longer needs an ASCII alphanumeric run to be evaluated (#B602 - `!!!`/`é€` never matched; through `$` tokens the gate saw the token NAMES, so it bit the plugin path and literal conditions only). The client `query` body: a late-resolved token splices twice-escaped and `decodeSplicedQuotes()` restores each spliced literal instead of deleting every escaped quote - byte-identical for any body whose strings carry no backslash (token-wise, so key order survives); an AUTHORED quoted segment holding a valid JSON escape is the one divergence (decoded). **One `$`-token grammar at every site (0.6.33, #B603/#B604/#B606):** `FormValidatorUtil.substituteFieldTokens(text, names, resolve)` — a token is `$` + a field name in scope, the LONGEST name wins in one pass over the original text, a token ends where its name ends provided the next character is outside `[A-Za-z0-9_-]` (so `($a) === ($b)`, `$a===$b`, `$a,` resolve while `$passwordX` leaves `password` alone), names are case-sensitive and RegExp-escaped (`$pw[0]`, `$a+b` resolve), a replacement is never scanned again, and a `$` naming no field stays literal. Before it: the plugin's `getDynamisedRules` replaced one name at a time and re-read the text after each splice, so a value holding `$<otherField>` was substituted twice (#B603 — and the two-argument `is` form re-read the resolved condition through the array-rule scan, which now skips `is`/`is<N>`); its DOM-fallback second loop, which could only act on a `$` a spliced value carried in, is retired with #B234's gate; the engine's `is()` resolved a token only when whitespace or the end followed it and interpolated names unescaped (#B604) — it now resolves OUTSIDE string literals only and never inside a regex-literal condition, so the plugin path's already-spliced literals are not resolved a second time; the client `query` body scanned `\$[-_\[\]a-z 0-9]+` — lowercase-only with a space in the class — so `$passwordConfirm` resolved `$password` + `Confirm` (another field's value on the wire), a prefix pair resolved by body order, and an unknown `$` was sent as the string `null` (#B606). Value semantics per site are unchanged (`getCastedValue`/`quoteForDynamisedRules`, `JSON.stringify`, the twice-escaped `query` splice). 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.) Since #M21d (2026-09-26) the browser compiles a custom validator as an inline `<script>` — `gina.forms.compiledValidators[<name>]`, one compile per validator per page, carrying the page's CSP nonce when one is set, `//# sourceURL=<name>.js` in DevTools — with NO `eval`, NO `Function` and no `'unsafe-eval'` needed; the scope contract is unchanged (the spliced prologue hands the body `self`/`local`/`isGFFCtx`/`replace` through `this.getValidationContext()`), a file the browser cannot compile throws `[UserFormValidator] Could not evaluate` pointing at the console's SyntaxError, and server-side a FUNCTION registered on the context is attached as it is while a source-shaped one is refused naming the browser-only contract. The bundle build (`core/asset/plugin/lib/js/no-dynamic-code.js`, run by `build` after r.js and again after Closure) also rewrites the two unreachable vendored calls the r.js output inlines — RequireJS `req.exec` (the `load.fromText` transpiler-plugin path) and engine.io-client's `Function("return this")()` global shim — by EXACT match, failing the build if a dependency bump moves them, so the published bundle carries no dynamic-code call at all: Socket's `Uses eval` verdict, which flipped the package score 62 ↔ 42 on identical bytes, has nothing left to fire on (pinned by `test/lib/validator-m21d.test.js` + `test/core/bundle-no-dynamic-code.test.js`). 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). **#FIN4 (`isIban` / `isBic`, 0.6.x):** IBAN and BIC join the built-in rule set. `isIban` validates ISO 13616 in three ordered checks - shape (`^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$`), official per-country length via the constructor-scope `_ibanLengths` registry (87 countries; unknown country codes deliberately pass on shape + checksum so a registry gap never hard-fails a well-formed IBAN), and the ISO 7064 MOD 97-10 checksum folded modulo 97 in <=7-digit chunks of plain Number math (no BigInt in bundled source - build-chain constraint). Tolerant read: validation runs on an uppercased copy with spaces/hyphens stripped; the stored value and the DOM are NEVER mutated (unlike `isEmail`'s lowercase write-back). The type guard folds into `isValid` (`typeof(candidate) == 'string' && rgx.test(...)`) so a toString-spoofing object records an invalid instead of throwing into the rule driver (#B200 posture, tightened). `isBic` validates the ISO 9362 8/11-char shape case-insensitively by regex - no case mutation. Both follow Shape A (`this.valid = isValid && !errors['isRequired']`), the strict #B199 empty bypass, and register English defaults in `_defaultErrorLabels` ('A valid IBAN is required' / 'A valid BIC is required'), culture-localisable like every built-in label. Behavioral + pin coverage: `test/lib/validator-isiban-isbic.test.js` (real-engine; fixtures verified against an independent BigInt oracle). **Autofill signal (#B478, 0.6.27):** the live check re-runs only on the proxied native `change`/`keyup`/`focusin`/`focusout`, and a browser or password-manager fill produces NONE of them - on Chrome not even at fill time, and the filled VALUE stays withheld from script (`.value` reads `''` while `:-webkit-autofill` matches) until the first trusted gesture, which then releases it and fires a keydown/input/change/keyup burst per field (measured, Chrome 152 - which is also why a "gated" trigger always submitted on the first click). So a form filled without keystrokes kept its bind-time gated look on a visibly complete form, and an autofilled INVALID value showed no error. Fix: the default stylesheet (`src/vendor/gina/autofill/sass/autofill.scss`, FUNCTIONAL so deliberately outside `@layer gina`; its keyframe carries a declaration because csso strips an EMPTY `@keyframes` block) gives every `:autofill` / `:-webkit-autofill` control a 1ms keyframe named `gina-autofill-start`; the validator's form-level `animationstart` proxy (`autofillProxyHandler`, also on reassociated controls) dispatches the field's own `change.<id>` live check when the value is readable, and calls `revalidateSilently()` when the browser withholds it. `excludeWithheldAutofill()` narrows the five LIVE-CHECK collectors (bind-time, live global, select global, reveal, silent) so a withheld field neither gates nor renders a false "required" error; the SUBMIT collectors stay strict, so `isValid()` still refuses to post a withheld (empty) credential. A consumer rule setting `animation-name` on those pseudo-classes replaces the hook - keep `gina-autofill-start`. Pins: `test/core/validator-autofill-livecheck.test.js` (extract-and-execute, dist + stylesheet contract) and `test/e2e/validator-autofill.spec.js` (the served keyframe applied through a harness class - real autofill is invisible to automation and to fresh CI profiles). **Honest label for a withheld autofill at submit (#B341, 0.6.27):** the submit collectors are strict by design, so a control the browser autofilled but still withholds fails `isRequired` at click time; that verdict is right, but the plain required label named a problem the user could not see. `isRequired` now consults the plugin's single withheld predicate (exported as `gina.validator.isAutofillValueWithheld`) and, when true, composes the message from the new built-in key `isRequiredAutofill` (default `Filled in by your browser: click the field to confirm it`) - the errors key stays `isRequired`, the verdict and the gate are untouched - and marks the control `data-gina-form-autofill-withheld="true"` until its next adjudication (a CSS/JS hook and the locale-agnostic test signal). `_labelAliasFill` copies an app-supplied `isRequired` onto `isRequiredAutofill`, so a localized catalog keeps its own wording until it translates the new key: never key a test on the English text. Pins: `test/core/validator-autofill-honest-label.test.js` (the rule, `replace()` and the alias fill executed from the shipped engine bytes; red-first through `GINA_FORM_VALIDATOR`) + `test/e2e/validator-autofill-honest-label.spec.js` (chromium + webkit; the plain label read off the page first, the honest render asserted to differ and to carry the marker). **Same-name pairs with a hidden twin (#B510, 0.6.29):** the painter walks `$form.elements` in document order and, on the per-field path, exits on the first control whose name matches — and a BARE `type="hidden"` control (no `form-item-wrapper`) can never be painted (every paint gate skips it). It used to consume the error anyway (box class set, no message, no `aria-invalid`, walk stopped), so the recommended disabled-control shape — a visible control plus a hidden twin carrying `"exclude": false` — lost its server-side field error whenever the hidden twin came first; on a shared box the pre-marked class blocked the visible twin's first-error branch even without the exit. A bare hidden control is now skipped when another control of the same name can be painted (`hasPaintableTwin`); a lone bare hidden keeps the former behaviour, a wrapped hidden is still painted after its wrapper. Browser-bundled: pickup is a bundle restart AND a re-bake. Tests: `test/e2e/validator-hidden-first-paint.spec.js` (real bytes, seven shapes) + `test/core/validator-hidden-first-paint.test.js`. **Binding scope (#B549, 2026-09-17):** the boot scan binds ONLY opted-in forms — a `data-gina-form-*` attribute, an existing id naming a registered rule (`-` read as `.`), or a virtual `gina-upload-*` id; any other `<form>` is left untouched (no minted id, no `$forms` entry, no submit proxy) even on a rules-bearing page, matching the forms guide. Explicit `validateFormById()` / `getFormById()` stay ungated opt-ins. Before the fix the scan's `else` arm bound every form after minting it a generated id, so declaring ONE rule file turned every plain login/signup form on the site into an always-XHR submit — and on isaac HTTP/2 that XHR-answered `req.login()` then lost its session cookie (#B550). **Staged-upload failure paths (gh#83, #B724/#B725/#B727/#B728/#B731, 0.7.2):** (a) #B727 — `onUpload` writes a staging error into the `-error` element as `textContent` inside a created `<p>`, never `innerHTML`: a non-2xx non-JSON body is kept verbatim as `result.message`, so a proxy/WAF HTML page or a reflected filename used to be parsed as live markup (the sink dated from 0.1.1). (b) #B724 — a status-0 settle reports `{status:408, transportError:true, reason, error: a11yLabel('transportError')}`; the text is 'Transport failure: the request did not complete' (a proxy 413 mid-send over HTTP/2 and a navigation abort both DID reach the server), overridable through `gina.config.a11y.transportError`; `reason` is `'unload'` while a `pagehide` is in effect, else `'transport'` — #B728 resets that flag on `pageshow`, because a back-forward-cache restore keeps the page's JS state. (c) #B725 — both payload collectors (`getFormValidationInfos` and the submit proxy's inline loop) skip a `type=file` control carrying `data-gina-form-upload-action`: it stays in `$fields` for rule evaluation, but its `.value` (`C:\fakepath\<name>`) never rides the payload. ⚠️ The default error render runs ALONGSIDE a declared `data-gina-form-upload-on-error` callback (element first, then `error.<vid>` and the callback), so a callback cannot suppress it. (d) #B731 — the error element is OPTIONAL: without it the message is announced through the form's live region (`announceA11yError`, when non-empty), a dev-mode `[FormValidator] upload … no error element` warning names the missing id, and `error.<vid>` and the callback still run; `onUpload` used to `throw new Error(errMsg)` there, with `errMsg` undefined outside the rendering branch, BEFORE either dispatch, so the callback never ran, for an HTTP error and a status-0 failure alike. The slot id is read ONCE, from the file input (`data-gina-form-upload-error`, default `<input id>-error`): it was recomputed for every `<input>` of the real form and kept the LAST one's, so a custom slot was honoured only when the file input was the form's last input. Residual: the bind-time `checkUploadUrlActions` still looks its errors up only at `<id>-error`. Pin: e2e `validator-upload-error-slot-b731.spec.js` (5 arms, red-first on the pre-fix dist).
939
939
 
940
- 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. **Staged-part lifecycle + orphan reclaim (#B469):** a staged part has exactly TWO reclaim paths — the `movefiles()` unlink when `self.store()` publishes it, and the per-upload `autoTmpCleanupTimeout` deletion timer (`false`/0/empty disables it — the shipped default, and removing staged files is then the operator's own job). The timer is IN-PROCESS: a restart strands every part it was holding, and a part that never reaches `store()` (a request failing AFTER the multipart parse — the parse precedes routing, so auth/validation/quota failures have already staged; or an app consuming the staged file without `store()`) then has NO reclaim path at all. So an ARMED timer also arms a boot-time sweep at server init: each configured landing dir (the resolved global + every group `path`, deduped) is swept NON-recursively for `<32 lowercase hex>.part` REGULAR files (lstat, symlinks never followed; a client-named orphan predating the opaque staged naming is indistinguishable from an application file and is left alone — ⛔ that name gate is LOAD-BEARING, never widen it: `_sweepDirs` includes every group `path`, and a group `path` is ALSO the PERMANENT destination, so on a deployment whose group path is a permanent publicly-served storage root the pattern is the only discriminator protecting real user files; the mtime gate cannot help there, since permanent files are old by definition) older than max(timeout, 1h) — the floor keeps a SIBLING live process's in-flight part on a shared staging dir safe, since its mtime stays fresh while chunks land; ENOENT at any step is not an error (a sibling bundle sweeping the same dir, or the app unlinking its own part). The sweep REPORTS at `info` only when it removed something or hit errors (`upload orphan sweep: removed N staged part(s) across M staging dir(s)`) and at `debug` when it found nothing — so at the default `info` level a silent boot means no staged part was older than the gate, NOT that the sweep did not run; an operator expecting the line after a restart that had nothing left to reclaim (the boot that did the reclaiming having gone with its logs) will otherwise read a healthy no-op as a missing line. Disabled timer = NO sweep — the documented operator-owns-cleanup contract is unchanged. An application MAY unlink `req.files[].path` itself: `movefiles()` tolerates a vanished source (ENOENT) by design, and the armed timer already requires that tolerance. Rule: a group `path` is the PARSE-TIME LANDING (staging) directory, never where published files live — `store(targetDir)` or the group's storage `driver` owns final placement, and one key must never carry both meanings. **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` + `upload-orphan-sweep.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` (on Bun too, which reports such an error without a stack: the helper gives it one, #B654), and a failed copy's temp sibling is removed only once its stream has closed, before the callback — its creation can finish after the failure is reported (#B649); `Controller.store()`'s mover does not have this reap yet — its failure path still checks for the temp sibling and settles at once (#B657, open, code-read). 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`). **Generated hidden fields — the `preview` slot (#B459, 2026-09-08):** the client writes one hidden input per `mandatoryFields` entry (`name`, `group`, `originalFilename`, `ext`, `encoding`, `size`, `height`, `width`, `location`, `mime`, `preview`) into the real form, auto-creating any the form did not declare — and `preview` was auto-created like the others although its value is an OBJECT (`{location, uri, tmpUri, width, height}`), so a form declaring no `[preview][...]` sub-fields got a flat `<prefix>[i][preview]` input, the fill loop's shared assignment string-coerced the staging response's preview object into it, and the real form's submit posted the literal `[object Object]` — a GARBAGE-VALUED field, not an absent one, so a data audit keyed on the field's absence reads the opposite of the truth (key on the value). The trigger is a CONJUNCTION — an undeclared form AND a response carrying a `preview` key; with no key the skip clause fires and the flat input was removed, which is why the documented response shape (no `preview`) never showed it. Since #B459 the auto-create loop seeds that slot with an EMPTY sub-field map instead of an input — so the fill loop still visits the key (the nested thumbnail is rendered from inside it) and posts nothing for it, the documented field set — and the shared assignment never targets the preview slot. Declaring `<prefix>[i][preview][location|uri|width|height]` hidden inputs is the opt-in for persisting the preview: the name parser is last-bracket-only, nesting is decided by a separate `/\[preview\]/i` test on the full name, so declared sub-fields fill from the response's nested object and post as a real nested structure — unchanged by the fix. A deliberately declared FLAT `[preview]` input is filed under the map (`preview.preview`), never as the slot itself, so the coercion path had exactly one entrance. Server code that keyed on the PRESENCE of a `preview` field for an undeclared form must key on its value: garbage before, absent now. Browser-bundled ⇒ pickup is restart AND re-bake. Tests: `validator-upload-preview-slot.test.js` (11: source pins, the auto-create loop extracted from the shipped bytes and executed on jsdom forms, dist fidelity — 9/11 red-first through the `GINA_VALIDATOR_MAIN`/`GINA_PLUGIN_DIST` seams) + e2e `validator-upload-preview-fields.spec.js` (arm 02 red on the pre-fix bundle through its `B459_PREFIX_BUNDLE` route lever, both thumbnails pinned as the constraint). 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). **Per-call since #B475 (2026-09-05):** the fluent handle used to register `self.on('uploaded', cb)` and never remove it, so a second `store()` on the same controller — sequential or overlapping — re-invoked every earlier `onComplete` callback with the later result; it now delivers through the callback path (`start(target, files, cb)`), one delivery per call, with the same arguments and the same synchronous timing on the empty-upload and released-response paths. The `'uploaded'` event no longer fires from `store()` at all: since #B475 the fluent form delivers through the callback, and since #B480 the fluent guard mints the handle for ANY non-function `cb` — `null` included, mirroring `query()`'s `typeof(callback) != 'function'` guard — so `store(target, files, null)` returns the handle instead of starting an upload whose outcome was emitted to nobody; `onComplete()` throws a `TypeError` synchronously when given a non-function (fail-fast at the caller rather than an uncaughtException inside an fs callback), and the seven unreachable `else emit('uploaded')` arms are removed together with the undocumented event (nothing in-tree ever listened to it, and it was never a `@fires`). **`query()`'s fluent `onComplete()` carries the same registration guard since #B485 (0.6.28)** — it minted a deliverer for any argument, so `.onComplete(null)` / `.onComplete('oops')` registered silently and the `TypeError` fired at settle INSIDE the delivery wrapper's own try/catch, surfacing as a misleading « Controller Query Exception while catching back » 500 that blamed the application callback (measured on a real instance — not the uncaughtException first assumed); now a `TypeError` naming `Controller::query` at the caller's line, the call-argument minting guard (`typeof(callback) != 'function'`) unchanged, so `query(options, data, null)` still returns the handle. The other 22 unguarded `onComplete` registration sites (connector `.then` shims, emitter-`once` helpers) are censused under #B491 — query()'s was the only one whose throw was swallowed. Test: `test/core/controller-store-null-callback.test.js` (9: source pins censused on a comment-stripped copy with raw-text controls, plus behavioural arms on a real instance, all red-first validated).
940
+ 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. **Staged-part lifecycle + orphan reclaim (#B469):** a staged part has exactly TWO reclaim paths — the `movefiles()` unlink when `self.store()` publishes it, and the per-upload `autoTmpCleanupTimeout` deletion timer (`false`/0/empty disables it — the shipped default, and removing staged files is then the operator's own job). The timer is IN-PROCESS: a restart strands every part it was holding, and a part that never reaches `store()` (a request failing AFTER the multipart parse — the parse precedes routing, so auth/validation/quota failures have already staged; or an app consuming the staged file without `store()`) then has NO reclaim path at all. So an ARMED timer also arms a boot-time sweep at server init: each configured landing dir (the resolved global + every group `path`, deduped) is swept NON-recursively for `<32 lowercase hex>.part` REGULAR files (lstat, symlinks never followed; a client-named orphan predating the opaque staged naming is indistinguishable from an application file and is left alone — ⛔ that name gate is LOAD-BEARING, never widen it: `_sweepDirs` includes every group `path`, and a group `path` is ALSO the PERMANENT destination, so on a deployment whose group path is a permanent publicly-served storage root the pattern is the only discriminator protecting real user files; the mtime gate cannot help there, since permanent files are old by definition) older than max(timeout, 1h) — the floor keeps a SIBLING live process's in-flight part on a shared staging dir safe, since its mtime stays fresh while chunks land; ENOENT at any step is not an error (a sibling bundle sweeping the same dir, or the app unlinking its own part). The sweep REPORTS at `info` only when it removed something or hit errors (`upload orphan sweep: removed N staged part(s) across M staging dir(s)`) and at `debug` when it found nothing — so at the default `info` level a silent boot means no staged part was older than the gate, NOT that the sweep did not run; an operator expecting the line after a restart that had nothing left to reclaim (the boot that did the reclaiming having gone with its logs) will otherwise read a healthy no-op as a missing line. Disabled timer = NO sweep — the documented operator-owns-cleanup contract is unchanged. An application MAY unlink `req.files[].path` itself: `movefiles()` tolerates a vanished source (ENOENT) by design, and the armed timer already requires that tolerance. Rule: a group `path` is the PARSE-TIME LANDING (staging) directory, never where published files live — `store(targetDir)` or the group's storage `driver` owns final placement, and one key must never carry both meanings. **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` + `upload-orphan-sweep.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` (on Bun too, which reports such an error without a stack: the helper gives it one, #B654), and a failed copy's temp sibling is removed only once its stream has closed, before the callback — its creation can finish after the failure is reported (#B649); `Controller.store()`'s mover does not have this reap yet — its failure path still checks for the temp sibling and settles at once (#B657, open, code-read). 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). **`send()` reads nothing from the event being dispatched (#B758, 0.7.3):** a staged file's `group=` comes from the file input its virtual upload form stages for (`uploadProperties.uploadTriggerId` → that input's `data-gina-form-upload-group`, `untagged` when unset). It was read from the implicit global `event.currentTarget`, right only inside the input's own change dispatch (picker, dropzone), so a direct `$forms[<virtual id>].send(formData)` outside a dispatch threw (reported as a staging error, while the request still went out with no boundary and no group) and inside an unrelated dispatch took that element's group, silently; a no-data `send()` now sends an empty body instead of reading `event.detail.data` and throwing after `isSending` was claimed. e2e `validator-upload-send-b758.spec.js` (5 arms, red-first). **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`). **Generated hidden fields — the `preview` slot (#B459, 2026-09-08):** the client writes one hidden input per `mandatoryFields` entry (`name`, `group`, `originalFilename`, `ext`, `encoding`, `size`, `height`, `width`, `location`, `mime`, `preview`) into the real form, auto-creating any the form did not declare — and `preview` was auto-created like the others although its value is an OBJECT (`{location, uri, tmpUri, width, height}`), so a form declaring no `[preview][...]` sub-fields got a flat `<prefix>[i][preview]` input, the fill loop's shared assignment string-coerced the staging response's preview object into it, and the real form's submit posted the literal `[object Object]` — a GARBAGE-VALUED field, not an absent one, so a data audit keyed on the field's absence reads the opposite of the truth (key on the value). The trigger is a CONJUNCTION — an undeclared form AND a response carrying a `preview` key; with no key the skip clause fires and the flat input was removed, which is why the documented response shape (no `preview`) never showed it. Since #B459 the auto-create loop seeds that slot with an EMPTY sub-field map instead of an input — so the fill loop still visits the key (the nested thumbnail is rendered from inside it) and posts nothing for it, the documented field set — and the shared assignment never targets the preview slot. Declaring `<prefix>[i][preview][location|uri|width|height]` hidden inputs is the opt-in for persisting the preview: the name parser is last-bracket-only, nesting is decided by a separate `/\[preview\]/i` test on the full name, so declared sub-fields fill from the response's nested object and post as a real nested structure — unchanged by the fix. A deliberately declared FLAT `[preview]` input is filed under the map (`preview.preview`), never as the slot itself, so the coercion path had exactly one entrance. Server code that keyed on the PRESENCE of a `preview` field for an undeclared form must key on its value: garbage before, absent now. Browser-bundled ⇒ pickup is restart AND re-bake. Tests: `validator-upload-preview-slot.test.js` (11: source pins, the auto-create loop extracted from the shipped bytes and executed on jsdom forms, dist fidelity — 9/11 red-first through the `GINA_VALIDATOR_MAIN`/`GINA_PLUGIN_DIST` seams) + e2e `validator-upload-preview-fields.spec.js` (arm 02 red on the pre-fix bundle through its `B459_PREFIX_BUNDLE` route lever, both thumbnails pinned as the constraint). 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). **Per-call since #B475 (2026-09-05):** the fluent handle used to register `self.on('uploaded', cb)` and never remove it, so a second `store()` on the same controller — sequential or overlapping — re-invoked every earlier `onComplete` callback with the later result; it now delivers through the callback path (`start(target, files, cb)`), one delivery per call, with the same arguments and the same synchronous timing on the empty-upload and released-response paths. The `'uploaded'` event no longer fires from `store()` at all: since #B475 the fluent form delivers through the callback, and since #B480 the fluent guard mints the handle for ANY non-function `cb` — `null` included, mirroring `query()`'s `typeof(callback) != 'function'` guard — so `store(target, files, null)` returns the handle instead of starting an upload whose outcome was emitted to nobody; `onComplete()` throws a `TypeError` synchronously when given a non-function (fail-fast at the caller rather than an uncaughtException inside an fs callback), and the seven unreachable `else emit('uploaded')` arms are removed together with the undocumented event (nothing in-tree ever listened to it, and it was never a `@fires`). **`query()`'s fluent `onComplete()` carries the same registration guard since #B485 (0.6.28)** — it minted a deliverer for any argument, so `.onComplete(null)` / `.onComplete('oops')` registered silently and the `TypeError` fired at settle INSIDE the delivery wrapper's own try/catch, surfacing as a misleading « Controller Query Exception while catching back » 500 that blamed the application callback (measured on a real instance — not the uncaughtException first assumed); now a `TypeError` naming `Controller::query` at the caller's line, the call-argument minting guard (`typeof(callback) != 'function'`) unchanged, so `query(options, data, null)` still returns the handle. The other 22 unguarded `onComplete` registration sites (connector `.then` shims, emitter-`once` helpers) are censused under #B491 — query()'s was the only one whose throw was swallowed. Test: `test/core/controller-store-null-callback.test.js` (9: source pins censused on a comment-stripped copy with raw-text controls, plus behavioural arms on a real instance, all red-first validated).
941
941
 
942
942
  211. **Request-body parsing contract — verbatim JSON, `req.rawBody`, and crash-safe decodes (consolidates former #162/#163/#164; #B28/#B30/#B64/#B588–#B592).** `application/json` bodies (POST/PUT/PATCH) parse VERBATIM first — `JSON.parse(request.body)`, with a `decodeURIComponent` fallback ONLY when that throws — so the client's exact types and string contents survive (no double-decode, no `"true"/"false"/"on"/"null"` coercion, no bracket-key expansion; error disposition unchanged — POST/PATCH 500 only when both attempts fail, PUT warns). **Urlencoded and unlabelled bodies follow the standard form algorithm (#B588/#B589/#B592, 0.6.33).** The POST/PUT/PATCH site hands the body over VERBATIM (only the content-type-gated `+`→space and a leading-`?` strip run there) and `formatDataFromString` splits on `&`, splits each pair at its FIRST `=`, then percent-decodes the name and the value exactly ONCE. Before, the body was decoded as a WHOLE — at the site and again in the helper — BEFORE that split, so an encoded `&`/`=` inside ONE value OR name became a separator that could ADD or OVERRIDE any other field (`role=user&bio=hi%26role%3Dadmin` → `role=admin`; a NAME carrying `%26role%3Dadmin` did the same), a raw `=` cut a value short (`a=b=c` → `b`), and a value was decoded up to four times (`100%2525` → `100%`). Now: a value leaning `{`/`[` is parsed as JSON when it parses — JSON carries its own types, so a quoted `"true"` INSIDE it stays a string (it was coerced) — and kept verbatim when it does not (it was dropped); a JSON value under a bracket key nests like any other value; bare `true`/`false`/`on`/`null` stay strings (they always did); raw quotes are kept (`Turn it "on"` no longer becomes `Turn it true`); a value-less segment is dropped and a repeated key keeps the last value (unchanged). The `"true"`/`"false"`/`"on"`/`"null"` casting is a DOCUMENT feature: it runs only when the whole input is a `{`/`[`-leading document or an object (stringified first — the browser validator's contract). A document is NEVER percent-decoded (#B589): the GET/HEAD re-parse used to decode the whole serialized query, so a value whose TEXT held `%22`/`%0A`/`%5C` made the document invalid and dropped EVERY query parameter (`req.get = {}`, and a `validator::` requirement copying that data answered 404); a fully `%7B`/`%5B`-encoded document is still decoded once. Isaac's query parser now decodes the NAME once as well (`+`→space first, as express's `qs` always did), so an HTML-form `user%5Bname%5D` still nests without the round-trip's accidental decode. A top-level `__proto__`/`constructor`/`prototype` name is dropped on both paths (#B592 — a JSON-valued `__proto__` swapped the PROTOTYPE of the parsed object); a route declaring a DTO re-parses its validated payload through this helper, so a top-level `prototype` key in its JSON body is now dropped too, while a plain `application/json` body is parsed as-is and keeps it. Parse failures log metadata only, never the input and never `err.message` (a `JSON.parse` message can quote the input — V8 and JavaScriptCore both do): the helper's `[365]` line logs the input's length, its leading character and `err.name`, and isaac's not-JSON warn the parameter's name, the value's length and `err.name` (#B590). The helper ships in the browser bundle, so pickup is a restart AND a re-bake. Suites: `test/lib/request-parsing-b588.test.js`, `test/lib/request-parsing-logs-b590.test.js`, `test/integration/container-boot-request-parsing.test.js` (a real boot, red-first against the pre-fix tree). **XML bodies pass through VERBATIM (#FIN1).** `application/xml`, `text/xml` and the `application/*+xml` suffix family fork off the legacy path on POST/PUT/PATCH before any decode: `request.body` stays the exact document (the same string `rawBody` carries) and the method slot is left unset — the routing loop normalises it to `{}` further down, so a controller still reads an object from `req.post`. The framework never PARSES XML, so it takes on no XML-parser surface and no XXE exposure; consumers bring their own library. Pre-#FIN1 such a body was silently DESTROYED rather than rejected: the bracket-notation data helper splits on `&` then on `=` and keeps only the first pair, so an XML declaration alone collapsed a whole document to a single bogus key which — having a key — then REPLACED `request.body` at the shared tail, while the `"true"`/`"on"` coercion separately rewrote quoted attribute values. The matched set is the convergent one — ASP.NET Core registers ApplicationXml/TextXml/ApplicationAnyXmlSyntax (`application/*+xml`), Spring's `MimeType.includes()` documents `application/*+xml` as including `application/soap+xml`, and type-is matches structured suffixes generically — with the wildcard scoped to `application/` in all three, so `image/svg+xml` is deliberately NOT matched and RFC 7303's non-document types (`application/xml-dtd` §9.5, both `xml-external-parsed-entity` §9.3/§9.4) are excluded. RFC 7303 §9.6.1 registers the suffix but RFC 6838 §4.2.8 makes it a naming convention that does NOT mandate generic processing, so this set is convergent implementation practice, not a normative requirement. ONE flag computed at the request prologue feeds all three branches (the #B103 single-site lesson, after those same three sites drifted); each arm seeds `obj = {}` because the shared POST/PATCH tail reads `typeof(obj) == 'object' && ownCount(obj)` and `typeof null === 'object'`. Both engines share the chokepoint; server-side only, no dist rebuild. **#B546 (0.6.31) — that tail, and every other own-property count over a CLIENT-KEYED container, goes through a module-local `ownCount()` rather than `<container>.count()`.** gina installs `count()` on `Object.prototype`, so the shorthand is an ordinary property lookup that an OWN field of that name SHADOWS, and a request carrying a top-level `count` made the framework call a string: a 500 on the guarded body branches, and an uncaughtException that EXITED the bundle process on the UNGUARDED query ones — unauthenticated, one request, on ANY url including one matching no route, because this parse precedes routing (driven live both ways: every shape reproduced before the fix and answers cleanly after). `ownCount()` is that same helper reached as `Object.prototype.count.call()`, which no own property can shadow, plus an explicit null/undefined branch that PRESERVES the throw the `obj = {}` seeding above depends on — the bare `.call(null)` would bind `this` to the global object and return ITS key count (measured 23) instead of throwing. Nested keys and near-misses (`counter`) were never affected. The framework's own server-to-server client (`self.query()`) sends raw `JSON.stringify(data)` for JSON bodies (RFC5987 value-encoding is for header values, not request bodies), and the inter-bundle HTTP/2 proxy no longer copies the INCOMING request's Content-Type onto the outbound JSON body it serialized itself — a urlencoded label re-routed raw JSON through the receiving parser's urlencoded branch and corrupted string values (the only incoming-CT→outbound copy framework-wide; kept for non-JSON bodies). `request.rawBody` snapshots the exact unparsed body (reference assignment, always-on, non-multipart only, `''` for empty) immediately BEFORE parsing mutates `request.body` — inbound-webhook HMAC verification needs the raw bytes, which middleware cannot recover after the stream drains; both engines share this single `core/server.js` pipeline. Malformed percent-escapes (`%`, `%zz`, truncated `%E0%A`) in a URL or query no longer kill the bundle (#B30 — an unauthenticated single-request DoS: any unguarded `decodeURIComponent` OR `decodeURI` on the request path threw `URIError` → uncaughtException → SIGTERM): redundant SECOND decodes on the GET/HEAD branches were DROPPED (the engine query parser decodes once, guarded — and since #B589 the data helper never percent-decodes the serialized query document at all), and every genuine first decode of attacker-controllable input routes through `safeDecodeURIComponent`/`safeDecodeURI` (try/decode/fallback-to-raw) — including the ERROR paths, where a malformed-% URL to a missing asset previously crashed FROM the error handler, turning a would-be 404 into a bundle kill. Sweep rule: `decodeURIComponent` and `decodeURI` are the same throwing family — sweep BOTH; fix the throw sites, never widen the uncaughtException net. Sibling #B64 (0.5.7): the two static-file resolvers confine the resolved filename to the matched mapping target (`path.resolve` both sides + separator-aware containment), so `../` / `%2F` / `%2e%2e` cannot escape to sibling files — an escape 404s exactly like a missing file. Second site, same guard (#B179, Security/CWE-22): the dev-mode Inspector SPA handler (`/_gina/inspector/*`) built its filename the same way — request target minus the prefix, joined onto the asset root, passed through the `_()` path helper — and `_()` calls `Path.normalize`, which **RESOLVES** `..` rather than rejecting it, so a literal `../` read any absolute path the bundle process could reach (verified live: 200 on `/etc/passwd`; encoded `%2e%2e` unaffected, since this handler never decodes — and decoding was deliberately not added). Node does not normalise the request-target (only clients do), so a raw HTTP client reaches it where a browser cannot. Unlike the static resolvers, this handler is **DUPLICATED per engine** (server.js + server.isaac.js) and `confineToBase` is scoped inside server.js's `Server()` closure — unreachable from isaac, which therefore declares an engine-local twin, pinned against drift by a test asserting both extracted copies agree. **Rule: any handler joining request-derived segments onto a base MUST confine the RESOLVED path; path normalisation is not a traversal defence, it is the mechanism that makes the escape land.** Corollary for endpoint families: check the SIBLING handlers' gating too — the Inspector endpoint carries no IP allowlist while its `/_gina/*` neighbours (`info`, `cache/stats`, `cache/clear`) all gate on `lib.admin.isClientAllowed`. **GET/HEAD query serialization no longer destroys escapes (#B407, consumer-reported):** both branches round-trip `request.query` through JSON.stringify + a re-parse, and the former textual nested-JSON unwrap ended with a blanket backslash strip over the WHOLE serialized document — so `%0A` in a value became the letter `n`, a backslash vanished, and a double quote OR a JSON-ARRAY value produced invalid JSON that silently dropped EVERY query param on the request (`parseBody` logs `[365]` and returns undefined). Both sites now serialize via `serializeQueryForReparse` (a shared inner helper in `processRequestData`): each own STRING value leaning `{`/`[` is unwrapped by PARSING it (kept verbatim when it does not parse — previously that shape also dropped the whole query), then a clean stringify with no post-processing; the whole-value `"true"/"false"/"on"/"null"` coercion is unchanged and, with escapes intact, can no longer match an escaped occurrence embedded inside a longer value. Intended behaviours preserved byte-for-byte (nested-object unwrap — now also correct for nested content carrying escapes, which the textual unwrap corrupted as well — coercion, plain values); array-valued and unparsable-`{`-leading params are delivered instead of killing the query. **PROTOTYPE-POLLUTION GUARD (#B446).** Every parse path that nests a bracket-notation key routes through `parseLocalObj` in `helpers/data` (aliased `nestBracketNotationKey`, and reached from `formatDataFromString`), which assigned `obj[key[k]]` from a CLIENT-SUPPLIED field name with no key filtering: `__proto__[x]=y`, `constructor[prototype][x]=y` and the percent-encoded `%5F%5Fproto%5F%5F[x]=y` each wrote to `Object.prototype` process-wide while the parsed body still read `{}` — i.e. SILENTLY. Reachable from the GET query string, urlencoded bodies, `inheritedData`, and multipart text-field names. ✅ **CONFIRMED LIVE over real HTTP on the DEFAULT `isaac` engine** (daemonless throwaway bundle, pre-fix worktree vs fixed tree, same fixture): a single unauthenticated `GET /?__proto__[polluted]=OWNED` — no body, no interaction — returned `polluted:"OWNED"` with `hasOwn:false` (the prototype-pollution signature, not an own property) and `arrayToo:"OWNED"`, and **the NEXT, innocent request inherited it at the same pid**, proving cross-request persistence. All three vectors reproduced; the fixed tree answered clean on all three with benign bracket nesting still HTTP 200. ⚠️ Isaac does NOT reference these functions at all — its own query parser is flat (`request.query[a[0]] = a[1]`); the attack survives because `server.js` re-serialises isaac's flat query (`serializeQueryForReparse`) and re-parses it through `formatDataFromString`, which restores the bracket branch. ⚠️ Instrument trap that nearly produced a FALSE NEGATIVE: `curl` treats `[...]` as a URL glob range, so every bracket request fails inside curl and never reaches the server — use `curl -g`. A second, independent source: PUT merges `JSON.parse` output as the merge SOURCE, and `JSON.parse` produces an OWN `__proto__` key, so an own-property check alone does NOT stop it — see #321. Measured gadgets before the fix: outbound-request options fully controllable (host/hostname/port/method/auth, `rejectUnauthorized` forced false, `protocol` feeding a dynamic transport require), `SAFE_HTTP_METHODS[m] === true` classifying POST as safe, and `req.routing.csrfExempt` reading true — the last two ONLY via the JSON source, since the string source yields `"true"` and is correctly rejected by the strict compare. ⚠️ The `csrfExempt` one is UNRESOLVED, not confirmed on a live route: what was measured is that a routing object LACKING an own `csrfExempt` reads the polluted value, but `core/server.js:7674` builds routes as `csrfExempt: routing[name].csrfExempt || false`, creating an OWN key that shadows the prototype — while `lib/routing/src/main.js:220` sets it only when already defined, so a route built there can lack it. Which construction path a live `req.routing` takes was NOT determined. Both sources now reject `__proto__` / `constructor` / `prototype` path segments and DROP the field rather than throwing (a throw on the parse path would turn one bad field into a 500). The guard is kept INLINE in `parseLocalObj`, not a closure helper, because `test/core/validator-send-formdata-nesting.test.js` extracts that function's source text and evals it standalone to pin client/server parity — a helper call is a ReferenceError there. `helpers/data` and the validator copy are both browser-bundled, so pickup is a bundle restart AND a re-bake. **#B591 (0.6.33):** `parseLocalObj` also carried a dead `obj = []` rebind for a numeric NON-LAST segment under a non-array container — the parent never saw it and the child slot it seeded was `null` — so `0[a]=1` threw a TypeError, and from the unguarded GET/HEAD `inheritedData` parse that EXITED the bundle (one unauthenticated request; measured exit 143). The rebind is gone in both copies: such a segment now creates a plain `{}` slot (`0[a]=1` → `{"0":{"a":"1"}}`), and no previously non-throwing result changed (every path through the rebind threw). Suite: `test/lib/bracket-nesting-b591.test.js`. Regression suite: `test/lib/prototype-pollution-b446.test.js` (attack arms each paired with a benign control, pre/post A/B validated on extracted HEAD source).
943
943
 
944
944
  212. **Structured (JSON) logging — `GINA_LOG_FORMAT=json` + per-request `requestId`/`durationMs` (consolidates former #152/#155; #M12a/#M12b).** The logger resolves its render format ONCE at init into `opt.format` (precedence `GINA_LOG_FORMAT=json|text` > `GINA_LOG_STDOUT` truthy ⇒ json > `text` default) BEFORE containers are cloned; a JSON line is `{ts, level, bundle, message, group, msg}` — `bundle`/`message` canonical, `group`/`msg` retained as additive back-compat aliases — and the raw `console.log` path honours the format too (otherwise JSON mode would interleave plain lines and break a collector); the `text` default keeps container logs byte-identical. Per-request `requestId` + `durationMs` ride JSON logs via a NARROW `AsyncLocalStorage` (`process.gina._reqALS`, parked on `process.gina` so it survives dev require-cache busting), gated on JSON logging ONLY (text renders no id field, so the ALS would be pure overhead — the text path stays byte-identical/zero-cost): the id resolver honours a SANITISED inbound `X-Request-Id` (`/^[\w.\-]{1,128}$/`, regenerate-on-violation kills log forging) else `crypto.randomUUID()`; the `.run({requestId, startMs})` wrap sits at `handle()` — NOT the request-entry handler — because the `request.on('end')` boundary between them loses async context while `handle()`'s awaits preserve it (the original body became `_handleDispatch`; `handle` is a thin wrapper); HTTP/2 gets per-stream scoping free via Node's per-stream compat `'request'` event, NOT `session.on('stream')`; the JSON-assembly sites read `getStore()` and add `requestId` + per-line `durationMs`, gracefully absent for CLI/boot/off-request logs. `.run()`, never `enterWith()` (enterWith bleeds sideways across siblings). **MS1 (2026-07-24, 0.5.25):** the same id is now PROPAGATED beyond the process — every `self.query()` outbound path (the file-download proxy + both the HTTP/1 and HTTP/2 inter-bundle clients) forwards it as `x-request-id` (sourced from the resolved `req._ginaReqId`, never a raw inbound header; a caller-set value always wins), and `server.js onRequest` ECHOES `X-Request-Id` back on every response — ungated / independent of `GINA_LOG_FORMAT` (the id is always-on even when the JSON-log field isn't) and guarded against an already-sent response — so one logical request stays correlatable as it fans out across bundles and a caller/LB/APM reads the id off the wire. `controller.js`/`server.js` are server-side → no dist rebuild; restart to apply. Tests: `test/core/request-id-propagation.test.js`. **The contextless `require('gina')` boundary + MQ-speaker transport resilience (moved here from the DTO entry that surfaced it; #B276/#B277 0.6.4, #B318 0.6.5, #B323 fixed post-0.6.5 — shipped 0.6.6 with the reconnect).** Importing gina outside a spawned bundle child has ALWAYS been an intended boundary (`index.mjs`'s own docblock states it; both published entries throw alike), but two defects sat on top of it. **(1) The throw was uncatchable and could HANG the process.** The logger is a load-time singleton whose default flows are `['default','mq']`, so `speaker.js` opens a TCP socket BEFORE the boot reaches its throw — and that socket kept the event loop alive (listener-CONTINGENT, measured both ways: nothing bound to the MQ port ⇒ ECONNREFUSED ⇒ exit 0, so the bug is invisible on a quiet machine and hangs with no output on one running bundles or in CI). Fixed by `client.unref()`: a logging transport must never be why a process stays alive. ⚠️ #B318: a THIRD state exists beyond connected/REFUSED — an UNREACHABLE host, where `unref()` covers the socket HANDLE but not the pending `TCPConnectWrap` REQUEST, which holds the loop by itself for the OS connect timeout (~75s on macOS); fixed with an unref'd connect deadline (2s default, `opt.mqConnectTimeout`) that `destroy(err)`s only while `client.connecting`. ⚠️ **That safety claim — "only while connecting" — is MEASURED FALSE and shipped a HIGH regression for one release (#B323):** `client.connecting` does NOT flip when the kernel completes the connect; it flips when the POLL phase runs `afterConnect` — and a timer fires in the TIMERS phase, which precedes poll. On any boot that blocks the event loop past the deadline (a bundle mount off a network filesystem — every Kubernetes-class start) the loop resumes, runs the OVERDUE deadline first, reads `connecting === true` on a connection the kernel established seconds ago, and destroys it; the bundle then logged NOWHERE for the rest of its life (the speaker dialled exactly once, ever). Measured both directions: a kernel-completed dial reads `connecting` TRUE at the timers phase and FALSE at the check phase; a black-holed one reads TRUE at both. The fix defers the verdict one phase — `setImmediate` INSIDE the deadline, so the read happens in the CHECK phase, after poll — a completed connect survives while a genuinely pending one still dies on schedule (#B318's no-hang contract intact). **Generalisable: a state flag written by an event-loop callback cannot be read from an EARLIER phase and treated as current** — `connecting`, `destroyed`, `readyState` and every `pending*` counter share the property, and a blocked loop is precisely what makes the stale read reachable; a timers-phase guard over I/O state wants a `setImmediate` (or the event itself), never a direct read. **The speaker also RECONNECTS (0.6.6):** `startMQSpeaker` returns a stable `{write}` transport FACADE (`mq/index.js` captures the return ONCE for the life of the process — without the indirection a replaced socket is unreachable); each dial builds its OWN `clientOptions` (the listener mints a fresh `sessionId` per connection and the handshake only fires while `clientOptions.sessionId` is unset — a shared object would make every reconnect skip its acknowledgement); `close` on the CURRENT socket (a `current !== client` guard keeps a superseded one from stacking a second connection) arms a capped unref'd backoff (`min(500 * 2^n, 30000)`, reset on connect). Unref'd is load-bearing BOTH ways: a short-lived CLI still exits while a long-lived bundle keeps retrying and HEALS if a listener appears later. The caller callback settles ONCE, the warn fires once per OUTAGE (not per retry), and frames are DROPPED while down — an unbounded queue behind an outage of unknown length is a memory leak, and the `default`/stdout flow carries the same lines regardless. ⚠️ The socket still CONNECTS — `unref` changes only whether it votes on process lifetime (consumer-verified against a live listener: the connected line still logs; "unref'd" is not "suppressed"). ⚠️ The workaround `GINA_LOG_STDOUT=true` does splice `mq` out of the flows but ALSO flips log format to JSON (measured) — a container logging-mode flag pressed into service as a don't-open-a-socket switch. **(2) The boundary's error now names the boundary** — the old message named an internal call (`setPath("gina.home", path): path cannot be empty`) because `getEnvVar` reads `process.gina` ONLY and is empty outside the CLI; it now routes the reader to `SuperController.createTestInstance()`, the supported way to exercise controller code without booting. ⛔ **Do NOT "fix" the boundary by extending the `getEnvVar → process.env` ladder to `gna.js`, nor by skipping the empty `setPath`** — REFUTED BY MEASUREMENT: satisfying that call merely defers the failure 8 lines to a bare-global `ReferenceError` (strictly less legible), and that `setPath` is the SOLE registration site for `gina.home` repo-wide (the SQLite connector and session store read it). Failing fast at the first detectable point is correct. Diagnostic instrument for the hang class: `process._getActiveRequests()` reports `TCPConnectWrap` while `_getActiveHandles()` shows only the unref'd Socket. Tests: `test/lib/logger-mq-speaker-resilience.test.js` (§01 deferral-ordering + reconnect-shape pins; §02 a child that blocks 3s right after the dial and must still deliver — red-first: pre-fix it lost the frame while the connection was ESTABLISHED, the defect's own signature; §03 a listener destroyed and rebound must be heard again; §04 the no-listener child still exits promptly) + `logger-mq-speaker-unref.test.js` (comment-stripped source pin — subtracting the fix left a bare existence pin GREEN against the commented-out line — + a behavioural arm with its own must-be-killed control). **Log redaction (#B433, 0.6.19):** every message is redacted BEFORE it is rendered and dispatched — inside the logger's `emit()`, the single pre-render point all 46 URL-bearing framework sites pass through (both engines, every render delegate, the CSRF filter; all levelled, none `console.log`), plus the raw `console.log` path — so stdout, the MQ speaker/`gina tail`, the file transport and the Inspector taps all receive the same masked line and a JSON line cannot be corrupted (the marker lands inside the message string). Configured per bundle in `settings.json > log.redact` `{enabled, defaults, secrets, patterns}`, ON by default: `defaults` = JWT · URL userinfo password · `Bearer`/`Basic` · named credential query keys (`token`, `access_token`, `api_key`, `secret`, `password`, `signature`, `otp`, …, key kept / value masked) · api-key headers — measured 0 false positives on 433k real log lines, ~1 µs per line, linear patterns; `secrets` = every value the secrets resolver substituted for a `${secret:KEY}` placeholder (settings, connectors, routing — one resolve point) masked verbatim, values < 8 chars skipped with a boot warn; `patterns` = consumer rules (regex source string → `[REDACTED]`, or `{pattern, flags, replacement, name}` with `$1` kept), `g` always enforced. Deliberately NOT a default: a bare long-hex path segment (a sha256 content-address key from storage and an opaque 64-hex credential are the same regex class — the consumer adds `(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])` — anchored on the class, not `\b`, which never fires against a prefixed `key_<hex>` segment). Fail-closed: an unknown key, a non-boolean flag, an invalid or empty-matching pattern refuses the boot (a dropped rule is a leak). Installed by `core/config.js::loadBundleConfig` right after the bundle's `secrets.resolve()` — the earliest resolved point, so connector-init lines are already covered — via `console.setRedaction(block, {group, secrets: lib.secrets.getResolvedValues(conf)})`; state lives on the logger's persisted context (survives `refreshCore()`), keyed by bundle, UNION across bundles (one bundle enabling is enough in a merged process; a per-route `logUrl:false` flag was rejected by measurement — it cannot reach the second URL copy inside the #ERRREF error stack). The 404 ref line carries the URL twice in ONE record (prefix + the error's own message); both copies are masked. Restart to apply, no re-bake (server-side only). A per-route or per-site flag is NOT the extension point; the logger seam is. Error rendering (#B434, 0.6.19): an `Error` passed as a log argument renders via `util.inspect` — message, stack, own props, `[cause]` chain — at BOTH stdout writers (the levelled `write()`/`parse()` walk and the raw `console.log` `JSON.stringify` path each saw only ENUMERABLE own props, so a bare Error rendered `{}`), top-level and nested alike, realm-safe (`util.types.isNativeError` beside `instanceof`), and the rendered message still crosses the redaction seam (content is built in `write()`, redacted in `emit()`). Non-Error arguments render byte-identically to before. **The `file` log sink — an IN-PROCESS transport, and the three defects that made it unusable (#B523/#B526–#B531, 0.6.30).** The opt-in `file` container connected to the MQ, received every line and wrote NOTHING: `setup()` was driven from `processProperties.bundles`, which `lib/logger/src/helper.js:125` fills only when `process.argv` matches `bundle:(start|stop|restart)` — while `core/gna.js:294` splices `process.argv` down to `[node, appPath]` in every bundle process loaded through the CLI, so that list is structurally always empty and no filename was ever assigned. Fixing the filename exposed what the dead sink had been hiding, measured on a live daemon topology: **(a)** the container dialled the MQ port at construction and ran `process.exit(0)` from its socket error handler, and in the daemon that dial happens BEFORE the daemon's own listener binds — so `gina start` died before "Framework ready" 3/3 whenever the flow was enabled, and a daemonless bundle exited with status 0 one second after boot; **(b)** the listener FORWARDS every speaker's line to every `writeToFile` session, so every process carrying the flow wrote EVERY bundle's lines into one shared per-host file (measured twice-over with two bundles) and built a full `Config` for a bundle it is not, which crashed `gina bundle:start` 3/3; **(c)** N processes appending to one file each kept their own rotation state and size counter, so rotation raced and lost lines silently. **The sink is therefore no longer MQ-coupled: it listens on the `logger#file` event `emit()` already raises for every flow, exactly like `containers/default`, and writes only the lines ITS OWN process logged.** No socket, no daemon dependency (it works under `gina-container` and in a container), no foreign `Config`, one writer per file. The file is named after the LOG GROUP — `<logdir>/<bundle>@<project>.log` — which is what makes "one writer" true; a group without `@` (the CLI's and the daemon's own `gina` lines) is not filed, matching the guard the old `write()` already applied. Records are written WITHOUT ANSI escapes, and `GINA_LOG_FORMAT=json` is honoured with the same line shape the stdout container writes. Rotation is configured under `rotate` in `~/.gina/user/extensions/logger/file/config.json` — the machine-level surface that also ENABLES the container (it reaches the container because `loadContainers()` merges `flowsOptions[flow]` over the logger options with override:true). Keys: `enabled` (default true), `when` (`daily`|null), `size` (default `10MB`), `count` (default 5), `maxAge` (e.g. `30d`, off). Mechanism is rename-then-reopen, never copy-then-truncate: the retired vendored rotator (MIT/Capriza, undeclared in either manifest so invisible to Socket and Dependabot, and unreachable — its `require` path resolved to a directory that does not exist) copied then truncated, losing every line appended during the copy window. ⚠️ The descriptor is opened SYNCHRONOUSLY and the stream wrapped over it: `createWriteStream(path)` opens asynchronously, so a burst of lines logged within one tick reaches a size trigger while the file does not exist on disk yet and the cascade's `renameSync` throws — measured with 60 lines in one tick. A size or age without an explicit unit is REFUSED, never guessed, and any invalid value disables rotation with a message naming key, value and consequence — reported THROUGH THE FLOWS (`process.emit('logger#'+flow, …)`, one payload per flow, which is what `emit()` itself does), never through raw `process.stdout.write`: under a daemon `lib/cmd/bundle/start.js` consumes the child's stdout and relays nothing after start, so the old raw-stdout refusal reached NOBODY (measured 0 in the daemon log, 0 at a tail client, 0 in the file, 0 in the CLI output). Logging continues either way. Two bounds stated honestly: bytes still queued when the process exits can be lost (`end()` is asynchronous, and making every write synchronous would let a full disk block the request loop), and a stream that stops draining past a 4 MB buffer drops lines with one warning per outage — the same posture the MQ speaker documents. ⚠️ **Generalisable trap that cost this arc most of its time: the repo-root `utils/prototypes.js` installs `Object.prototype.count` + `functionCount` and `Array.prototype.clone` + `inArray` ON REQUIRE, unguarded — so `typeof(anyObject.count)` is `'function'` for EVERY object including `{}`.** A `typeof(user.<key>) != 'undefined'` guard over a user-config object therefore hands the inherited METHOD back as the configured value — here `~~(function)` is 0 and rotation disabled itself reporting "count is undefined" on a valid default. ⛔ Do NOT cite or patch `helpers/prototypes.js:169`: it declares the same names, but its guard `typeof(Object.count) == 'undefined'` reads the INHERITED method once `utils/` has run (`Object` inherits through `Function.prototype`), so every declaration there is skipped — the trap defeats its own guard and that line is dead code. The properties are `enumerable: false`, so `Object.keys()` reports `[]` and `JSON.stringify` drops them, which is exactly why every probe read the key as ABSENT rather than as a function. ⚠️ An isolated repro is the CHEAPEST way to see this, not an impossible one — `node -e "require('./utils/prototypes.js'); console.log(typeof({}).count)"` prints `function` in a plain process with no framework and no invocation (control: without the require it prints `undefined`). What DOES false-negative is requiring the OTHER file: `helpers/prototypes.js` exports a constructor that must be CALLED, so a bare require of it installs nothing and reports every key absent — inside the framework tree too, which is why that probe reads like a legitimate in-context measurement. Pair any such probe with a known-positive control. ⚠️ **The same collision has a SECOND, REQUEST-FACING direction — #B546 (0.6.31):** the trap above is the framework READING a config key that the helper makes look present; the mirror is the framework CALLING `<container>.count()` on a container whose keys the CLIENT chooses (parsed body, query bag, route params, `req.files`, `self.query()` data, a routing rule's validator data, and `| length` on BOTH template engines). There an own field named `count` shadows the helper and the framework calls a string — see #211 for the impact and the `ownCount()` remedy, applied at 27 sites across `core/server.js`, `core/controller/controller.js` and the validator engine. Counts over FRAMEWORK-built objects (response headers, the routing table, config) deliberately keep the shorthand, and the helper itself stays on `Object.prototype` for application code. Use `Object.prototype.hasOwnProperty.call(obj, key)` for any own-key test over config in this framework, never `typeof` — the guard is name-independent, so it is right for all four names and any added later. **#B524 (0.6.30) — `gina-container` applies the container preset ITSELF.** Unless `GINA_LOG_STDOUT` is already set, the launcher writes `process.env.GINA_LOG_STDOUT = 'true'` ABOVE its `utils/helper` require — that require initialises the launcher's OWN logger, and an unconfigured container was measured running TWO redialling speakers (launcher + bundle), each warning `ECONNREFUSED 127.0.0.1:8125` once and then dialling every ≤30 s for the life of the container — and the bundle inherits it through the spawn; `image:build` images inherit it for free, their entrypoint being `gina-init && exec gina-container`. It MUST be a raw `process.env` write (`setEnvVar()` populates `process.gina`, which the logger never reads). Explicit values win both ways, measured on isolated boots: `GINA_LOG_FORMAT=text` ⇒ coloured text with the dial still skipped; `GINA_LOG_STDOUT=false` ⇒ the pre-0.6.30 behaviour byte-for-byte. ⚠️ LAUNCHER topology only: a bundle started through a framework daemon (`gina start` → `bundle:start`) is untouched — there the MQ transport is what `gina tail` reads, and the daemon DISCARDS the bundle's own stdout after startup (`bundle/start.js` `if (isStarting) return;`), so the relay is the only path a runtime line has to an operator. **#B691 (0.7.1) — the one exception, during the boot itself:** the MQ listener keeps NO backlog (`listener.js` `report()` forwards each frame to the tails attached at that instant) and `gina tail` cannot ask for earlier frames, so a tail attached after the boot — the ordinary `bundle:start`-then-`tail` order, and a container init script that tails after starting — saw none of a bundle's boot-time lines, including the 0.7.0 `autoescape` and unanchored-`requirements` warnings. The startup watchdog now passes the bundle's warn-and-above boot lines on to the `bundle:start` client (so also to `bundle:restart`, which prints its start step): `warn`/`warning`, `error`/`err`, `crit` and `alert` (the logger renders the name the caller used, and recommends `warning`/`err`); `emerg` keeps its `aborted :(` path. The filter is `lib/cmd/bundle/inc/boot-lines.js`: it buffers by line across chunks, reads the level from the ANSI-stripped `[date] [level ][group]` header or from a JSON line's `level`, keeps a multi-line entry whole until its colour block closes (`format()` wraps the whole message in one), and caps a line and an entry at 64 KB. It runs after the `isStarting` guard and before the EADDRINUSE/emerg/started checks, so a warning comes before `started V(-.o)V`, which then sits on its own line; with no such line the output is byte-identical. A tail attached first still gets the lines, so they show twice there (accepted). Runtime lines stay the tail's. Pickup = a DAEMON restart (`framework:init` requires a command module once), not a `bundle:restart`. Tests: `test/lib/bundle-start-boot-lines.test.js`. That topology's JSON answer is the OTHER half of #B524: **`gina tail` renders one JSON object per line when its OWN logger's resolved format is `json`** (`GINA_LOG_FORMAT=json` on the tail process — the pod's environment reaches it; `lib/cmd/framework/tail.js` reads `console.getOptions().format`, the logger's single precedence rule, and routes BOTH render sites — the delayed-message replay and the live stream — through one `renderLine(pl)`; same `{ts, level, bundle, message, group, msg}` shape, NO `requestId`/`durationMs` because the relay payload is `{group, level, content}` only). Measured on a daemon-free relay scene (a real `MQListener` on an isolated port + a `gina-container` bundle booted with `GINA_LOG_STDOUT=false` so it speaks + `bin/cli tail`): format unset ⇒ 5 ANSI lines, 0 JSON; `json` ⇒ 5 JSON objects, 0 escapes, the tail's own connect lines included. ⛔ `GINA_LOG_STDOUT=true` is NOT the daemon topology's switch — it never reaches a daemon-spawned bundle's logger (#B532: `bin/cli:336` `filterArgs()` MOVES every `GINA_*` key out of `process.env` before `bundle/start.js:404` spawns with no `env:`, and the child's logger initialises at `gna.js:183` BEFORE the ctx mirror at `:276` restores them — so the tutorial's `GINA_LOG_FORMAT=json gina bundle:start` cannot reach the bundle either, and a daemon-topology JSON line can never carry `requestId` until that is fixed), and if it did reach the bundle it would silence the relay the tail reads.
945
945
 
946
- 213. **Client-plugin discipline — CSP-safe listeners, native-dialog parity, preload coalescing (consolidates former #149/#159/#200).** (1) Client-bundle plugins must NOT inject inline event-handler attributes (`setAttribute('onclick', …)`, `el.onX = …`) — they trip CSP `script-src-attr` under nonce-based policies; suppress defaults with `addEventListener('click', e => e.preventDefault())`, and keep it `preventDefault`-ONLY (no stopPropagation) when the element participates in event delegation other handlers depend on — a `preventDefault`-only listener still sets `event.defaultPrevented` exactly as the inline handler did. (2) Dialog popins open as native modals (`$el.showModal()`) in EVERY env — the dev-only non-modal downgrade + manual overlay are gone for dialog mode (native `::backdrop`); a consumer that PRE-OPENS the dialog (skeleton loading) must also use `showModal()` or it is born non-modal with nothing positioning it; the opt-in `preOpen`/`loadingShell` skeleton is idempotent via `hasAttribute('open')` — NOT getAttribute truthiness, because `showModal()` sets `open` to the EMPTY string — and since #B574 (2026-09-20) `popinOpen`'s own re-entry guard reads the same `hasAttribute('open')`: it read getAttribute truthiness, never skipped a shell-opened dialog, and on the new trigger's default MODELESS resolution called `show()` on the still-modal shell, which throws `InvalidStateError` and left `isOpen` false on a visibly open popin; a native UA close (Escape, `<form method="dialog">`) fires the element's `close` event WITHOUT the plugin's own close path, so a once-per-element `close` listener (the event is `close`, not `cancel` — `cancel` is Escape-only) routes it through the plugin close, keeping open-state/listeners/toolbar consistent — a UA-closed shared popin otherwise re-opens "one render behind" (#B58); and since gh#76 §8 (2026-09-20) the `preOpen` loading shell is an explicit STATE — `$popin.isLoading` is true from the moment the shell shows until the real open (`isOpen` stays false, so the two `!isOpen ⇒ popinOpen` consumers still run it), backed by a per-popin load sequence and the in-flight transport (`_loadSeq` / `_loadXhr`): `close()` / `destroy()` on a loading popin cancel the load (`closeLoadingShell` — sequence bump, `abort()`, teardown by the shell's OWN state because the class-keyed teardown never matches a dialog shell, trigger release, `close.<id>`; the aborted transport's readyState-4 handler runs its release block and then drops on the sequence check, so a cancel fires no `error`), the native `close` sync is bound at SHELL time through `bindNativeCloseSync` (guard `isOpen || isLoading`) so an Escape during the load routes through the plugin instead of re-opening when the content lands (driven on the pre-fix bundle: Escape closed the dialog, the landing re-opened it), `loadContent()` on a loading popin injects into the DIALOG element (`target` still names the container until popinOpen re-points it) and completes the open through popinOpen, and a non-2xx load fires `error.<id>` FIRST, then closes the shell if nothing loaded content into it; a popin without `preOpen` never enters the state, and `getActivePopin()` stays open-only; and #B579 (2026-09-20): the declarative trigger's `loaded.<id>` listener applies a NON-STRING detail as nothing — `popinLoadContent`'s redirect branch fires `loaded.<id>` with the POPIN OBJECT as detail (the legacy listener only binds on it), so a `load()` landing on an already-OPEN `data-gina-dialog` popin wrote the new body and then wiped it, leaving the dialog empty with no error (driven on the published bundle; every version since 0.4.6). (3) A popin/dialog click fired TWO identical GETs because the hover/`focusin` preload — `focusin` is part of the click gesture — was never reused: an in-flight preload registry lets the click-time consume ADOPT the in-flight fetch (waiter woken with the body, caller's own load on failure) instead of fetching again, and the legacy click path consumes preloads at all now (#B54); only the initial GET is ever preloaded. The consume path bypassing the full load tail (redirect/JSON handling) was ASSUMED acceptable — but a hover/focus-WARMED trigger whose GET returns a redirect/JSON response (`application/json` `{isXhrRedirect,location}`) had that raw JSON blind-injected as the popin body via `applyContent` (`$el.innerHTML`), the `_self` tunnel never firing — IDENTICAL symptom to the #B77 `_self` timer race but a DIFFERENT mechanism (the consume path, regression `2c61c494` v0.5.5), so #B77 fixed only the UNWARMED click-time path and the WARMED path persisted until #B80 (2026-07-06, `37e6829c`): `preloadFetch`'s `onreadystatechange` now caches a 2xx response ONLY when its Content-Type is NOT `application/json` (mirrors `popinLoad`'s own `isJsonContent` detection), firing the in-flight waiters with `null` (→ `consumePreload`'s `onMiss` = the click-time `doLoad`) and leaving the cache empty (→ a ready consume returns false → `doLoad`), so a JSON-returning trigger falls through to the click-time `popinLoad`'s full redirect/JSON tail exactly as a non-preloaded click (one extra GET on click — the hover-warm GET is not reused; measured 2 GETs). Only a genuine HTML fragment is preloaded+injected. Browser-bundled → prod dist rebuilt; VERIFIED in a real browser (a warmed legacy `data-gina-popin-url` redirect trigger now shows the tunnel target, not the raw JSON; the decline path positively exercised via resource-timing) — jsdom is falsely green. That residual — the GET itself still fired on hover for side-effecting triggers, authenticated (same-origin cookies) and CSRF-token-less (GET) — is CLOSED by the #B91 per-trigger opt-out (2026-07-10): `data-gina-dialog-preload="false"` on the trigger (honored on legacy `data-gina-popin-url` triggers too — the gate reads the attribute off the closest-matched intent target; case-INSENSITIVE `/^false$/i` on purpose, so a templated "False" cannot fail open and fire the GET anyway — deliberately safer than data-gina-dialog-modal's case-sensitive parse) suppresses BOTH the hover warm and the focusin-on-click warm at the shared intent handler; the opted-out click loads normally at click time (undefined cache slot → consume returns false → the caller's click-time load; exactly one GET per click). Default stays preload-ON: GET is presumed safe to fire early per HTTP semantics — a side-effecting trigger declares itself (the hover-prefetch ecosystem's convention, live-verified). (4) A proxied/tunnelled redirect JSON response (`isXhrRedirect`+`location`, default `_self`) used to `$popin.load()` then arm a blind `setTimeout(50, () => !$popin.isOpen && $popin.open())`; a follow-up load slower than 50 ms opened the popin against a not-yet-injected (skeleton/empty) target → intermittent unhandled-deref crash. Removed as vestigial (v0.1.0, 2021) — the already-armed `loaded.<id>` listener opens CONTENT-FIRST (injects the body via popinBind/handleLoadedBody, THEN popinOpen), so the load alone suffices; the `_self` branch's `return;` still guards the `window.open` fall-through (non-`_self` targets only) (#B77). Event contract: `open.<id>` = a fresh open, `loaded.<id>` = reload/redirect content injected — a consumer needing "content ready on every load" must listen to BOTH. A structurally identical blind timer lived in the SIBLING validator `Validator::Popin now redirecting` path (`core/plugins/lib/validator/src/main.js`), removed by #B79 (2026-07-10): the validator DISCARDED popinLoad's return, and that return IS the `{open}` handle whose `open()` arms the content-first `loaded.<id>` listener — so the timer was papering over a race the discarded handle created (an XHR faster than 50 ms fired `loaded` into the void and the body was lost, a slower one opened an empty popin first). It now captures the handle and calls `open()` inside the same not-open gate, guarding the `undefined` popinLoad returns when the request cannot start (CORS unsupported) so a failed load never blind-opens an empty popin; and the cross-popin branch resets `isRedirecting` before closing the ORIGINAL popin, because `popinClose` early-bails on a redirecting popin — that close was a silent no-op, leaving the original open behind the new one (browser-verified: pre-fix stays open, post-fix closes). MEASURED CORRECTION: the timer was NOT load-bearing for the different-popin branch as long assumed — that branch threw first, because the published `gina.popin` WAS re-assembled with a target-wins merge on every `new Popin()`, so its `getPopinByName`/`getPopinById` stayed bound to the FIRST instance's registry and resolved ONLY the boot popin, while `$popins` + `getActivePopin` saw every popin and `gina.popin.activePopinId` never updated (so `getActivePopin()` returned null once nothing was open) — and popins registered AFTER an instance's publish (click-time in-page dialog registrations) never reached the published registry at all, with destroyed popins lingering in it. FIXED as #B90 (2026-07-10): ONE module-scoped registry (`_sharedPopins`) aliased by every instance's `$popins`; a publish-ONCE `gina.popin = instance` (the published object IS the first instance, LIVE — re-publishing was both the freeze defect and, with a shared registry, a self-merge recursion hazard: gina's merge deep-recurses plain objects and `$popin.eventData` can hold the $popin itself); and a `setActivePopinId` write-through helper at all 7 write sites keeping the published `activePopinId` truthful no matter which instance opens or closes a popin. Browser-verified both ways on the built bundle (pre-fix: accessors blind + the validator cross-popin redirect 422-throws `not found`; fixed: accessors resolve every instance's popins and a form submit redirecting into a DIFFERENT popin works end-to-end — the original closes, the target loads content-first, `activePopinId` follows). Generalises: an accessor published through a target-wins `merge()` silently keeps the first instance's closure — publish a live instance once and share module-scoped state instead of re-merging per construction; and never infer a code path is reachable from the fact that it exists. (5) EAGER content warm — `data-gina-dialog-preload="eager"` (case-insensitive) opts a trigger into a one-shot idle warm-all pass: after `window` load, on requestIdleCallback (setTimeout fallback), serialized one GET at a time, routed through the SAME shared per-trigger gate as the hover warm (`warmTrigger`: disabled skip → the `"false"` opt-out → URL-cache dedup → in-flight reserve + fetch) so the two warm paths cannot drift and whichever fires second is a no-op; skipped entirely under Save-Data; triggers injected after the pass keep the delegated hover warm; staleness matches the shipped no-TTL hover semantics — the opt-in accepts the wider warm→open window. (6) #B139 (2026-07-20, 0.5.22): the content cache no longer outlives the open it warmed — measured pre-fix it was a ONE-GENERATION-LAGGING store (every open after the first paid ~1 GET yet rendered the PREVIOUS open's fetch: the leftover entry — the #B54 in-flight adoption's repopulated body, or the around-open re-warm — blocked the fresh warm's dedup; NO invalidation path existed, close/unbind/destroy never touched the cache; reproduced back to 0.5.4, so it predates #B54 and the eager pass). Fix: close clears the popin's content-URL slot (stamped at consumePreload both branches + popinLoad) AND re-sweeps the same slot ~120ms later, because the close-time a11y focus-return and pointer re-hover fire TRUSTED synthetic intents that re-warm the slot with close-era content within 1ms of the close (measured; a raced sweep is benign by construction — at most one extra fetch, never staleness, since an adopted in-flight body still reaches its open through the waiter chain). AND `preload="false"` triggers now skip the cache READ at open on both open paths — `false` is the hard always-refetch spelling for volatile popins (its GET always happens at open, never from a same-URL sibling's warm); no new annotation vocabulary. Warmed-never-opened entries keep the page-lifetime eager semantics above; default triggers pay at most one extra idempotent GET per close (the swept synthetic warm). Content preload deliberately does NOT ride 103 Early Hints: popin responses vary on `X-Requested-With` (a non-XHR GET of a fragment layout gets the iframe-wrap variant and misses the scripts append), and a browser `Link rel=preload` fetch never carries that header — wrong-variant bytes or a double GET, with no success branch; `self.setEarlyHints()` remains the right tool for a popin's STATIC subassets (the CSS/JS it injects via getScript/getStyle). (7) #A11Y8 (2026-08-05, 0.6.4): the background-`inert` loop in `applyNonModalShims` skipped `instance.target` — the shared `.gina-popins` container — and since every popin lives INSIDE that container, and `popinOpen` never closes the popin it supersedes (it only overwrites `activePopinId`), opening a second NON-MODAL dialog left the first one fully keyboard-reachable behind it: a user could tab out of the dialog on screen and into a stale one. Non-modal is the FRAMEWORK DEFAULT for `data-gina-dialog` (`resolveModal` falls through to `return false` at step 5; only a legacy `data-gina-popin-name` trigger is unconditionally modal), so this was the ordinary path, not an exotic one — note item (2)'s "native modals in EVERY env" predates the non-modal branch and now describes LEGACY triggers only. Fix: descend into the container instead of skipping it, and inert sibling `dialog[open]` ONLY — a `<dialog>` without `open` is already `display:none` per the UA stylesheet, so it is unreachable without help (measured in Chrome with controls firing both directions; the modal path was never affected, native `showModal()` handles the top layer itself). Teardown needed NO change: `removeNonModalShims` sweeps `[data-gina-popin-inert]` document-wide, so dialogs nested in the container restore without it knowing they exist. The `getAttribute('inert') == null` guard means gina only marks what it set, so a consumer-set `inert` is never stamped and never stripped. All seven are browser-bundled — prod dist rebuild required; verify (2), (3), (4) and (5) with a REAL preemptive-open / preload / redirect-tunnel consumer — a minimal smoke page is falsely green; (7) needs TWO popins open at once, the only scene in which it differs at all. (8) **A trigger gate must not trust the native `disabled` ATTRIBUTE on an element the browser already enforces (#B296, 0.6.5).** The three DISPATCH-time gates (`openFromTrigger`, the `bindOpen` document proxy, the per-`$element` listener) now require `!('disabled' in $el)` before the attribute counts, mirroring the validator's `isTriggerDisabled` (#B293) so the two subsystems agree on what "disabled" means. Rationale, measured: a natively-disabled `<button>` delivers **0 clicks to JS**, so the attribute arm could only ever fire on a `disabled` written DURING the dispatch — the shape of every consumer double-submit guard — which ate the open and then cleared the attribute, leaving nothing marked and a normal-looking control; on an `<a>`/custom element/span the browser enforces nothing, so the arm is KEPT (`'disabled' in $el` → `button:true · input:true · anchor:false · custom-element:false · span:false`). It also cannot weaken gina's OWN re-entry guard: `armPopinTrigger` arms `<a>` with `aria-disabled` (untouched arm) and every other tag with native `disabled` — enforced by the browser for real controls, and still enforced by this gate for `<span>`-likes, which is exactly the branch the `in` test preserves. **Two gates are deliberately NOT changed:** the hover/focus `warmTrigger` preload gate (removing it there would start PRELOADING a `<button disabled>` nobody can click — if it ever changes it wants the IDL property `$el.disabled === true`, not the ignore treatment) and the bind-time `$link` skip (structurally unreachable by a mid-dispatch write, and it carries a different one-arm predicate). **Reachability is unequal and the difference is load-bearing:** at `openFromTrigger` gina's proxy is DELEGATED so a button-bound guard always wins the race (plain #B293 shape reproduces); at the per-`$element` listener gina binds to the NODE, so only a CAPTURE-phase or earlier-registered guard can poison it (measured both ways — a target-phase guard bound after gina's does NOT reproduce); the legacy document-proxy site is traced-not-exercised. Test `test/e2e/popin-trigger-native-disabled.spec.js` — a replica cannot catch this, it is an ordering interaction between a consumer listener and a delegated proxy. ⚠️ **Scene trap for anyone writing a popin close-button test:** the close element must carry NO `id` — gina assigns `popin.close.<n>` only to an id-less one and the dispatch gate matches on that prefix, so a consumer id makes EVERY arm read "did not close", subtract control included (that is its own defect, #B299). (9) **A teardown loop must never splice the array it is walking (#B265, 0.6.5).** `popinUnbind`'s validator-form teardown spliced `$popin['$forms']` inside a `for` bounded by a length captured BEFORE the loop, so each splice shifted the array left under an incrementing cursor and the read index skipped one every time — the originally ODD-indexed forms were never destroyed and were left behind in the very array the loop exists to empty. Because the popin's `innerHTML = ''` runs BEFORE the loop, their surviving validator entries pointed at already-detached nodes, and `validateFormById`'s "return existing when available" early return then handed the stale entry back on the next open, so the form and its submit trigger came up silently inert — the same end symptom as #B294 by a completely different route. Fix: snapshot with `.slice()`, clear once with `.length = 0`, iterate the snapshot. **Two things worth carrying beyond this bug.** First, `destroy()` is not free: it fires `destroy.<formId>` (validator `main.js:4241`) and `'destroy'` IS on the plugin's public registrable-event list, so making a skipped form destroy properly starts firing consumer `on('destroy')` callbacks that never fired before — a teardown fix is a behaviour change, not merely a leak fix. Second, per-form teardown is now wrapped: `destroy()` → `unbindForm` → `getFormById(vFormId)` THROWS for a form holding a `data-gina-form-virtual` file input once its virtual form is unresolvable, which is precisely the detached state every form is in at that point — and before the guard, one such throw silently abandoned every LATER form in the loop. **Reachability is much narrower than it looks and must not be overstated:** the loop is gated on a validator — since #B756 (0.7.2) the one the POPIN was registered with (`$popin.options.validator`, falling back to the closure's `$validatorInstance`); before, the closure alone, a PER-INSTANCE value assigned only from `options.validator`. Gina's own boot does `new Popin({name:'gina-dialog-boot'})` with no validator, and the delegated `data-gina-dialog` listener is installed once by that boot instance (module guard `_ginaDialogDelegated`), so **the declarative dialog path can never reach this code** — it needs a popin constructed with a validator (`new Popin({name, validator})`, the legacy popin API). #B756 was the closure gate's own defect: `gina.popin` IS that validator-less boot instance, so the documented `gina.popin.close(name)` skipped a validator-registered popin's teardown and the reopened form came up unbound (`validateFormById`'s early return again — a third route to the #B265 symptom), while the popin's own `close()` and its close button run the registering instance and always tore down. Pin: arm 04 of e2e `validator-upload-popin-reopen-b733.spec.js`. That is also why the test is a replica + source pins (`test/core/popin-forms-teardown.test.js`) rather than an e2e arm: an e2e scene on the declarative path would be VOID, every arm reading "nothing destroyed" for a reason unrelated to the defect. (10) **Never re-derive an element's ROLE from its id when the call path already proves it (#B299 + #B301, 0.6.5).** `register()`'s close branch gated on `/^popin\.close\./.test(event.target.id)` — a prefix gina mints at bind time — and that single line carried TWO silent defects: gina assigns the prefixed id **only to an id-less element** (`:1560`), so a consumer-supplied id is taken verbatim and matches nothing (**#B299**); and `event.target` is whatever was actually CLICKED, so an icon nested inside the button matches nothing either (**#B301** — the ordinary `<button class="gina-popin-close"><svg/></button>`, and by far the wider case). Both are invisible: `cancelEvent` runs at the top of the listener, so the button also swallows its own default — no error, no navigation, nothing. Fix: read **`event.currentTarget`** (the element `register()` bound — a `.gina-popin-close` by construction, since that function has ONE call site fed only from `querySelectorAll('.gina-popin-close')`) and treat a `popin.click.*` id as the sole exception, which preserves the dual-role element (a trigger that is ALSO a close button — reachable because `bindOpen` scans `document`, not just the host page). ⚠️ **What must NOT be "fixed" instead: rewriting the element's id.** The `$close` teardown sweep removes this listener via `gina.events[eId] == eId` — the degenerate name===id entry that `events.js:42` produces — so decoupling the event name from the id leaks the listener; and overwriting a consumer id would break their selectors, `aria-labelledby` and test hooks. Contrast `bindOpen:1195-1203`, which takes the OPPOSITE policy and does clobber a consumer id on trigger elements. Measured with controls firing both directions on the real bundle (id-less closes / consumer-id does not / nested click does not / patched all close), test `test/e2e/popin-close-dispatch.spec.js`. (11) **gina's own in-flight arm must be honoured by the path that dispatches it — a legacy trigger re-fired during its own load (#B298, 0.6.5).** `armPopinTrigger` marks an `<a>` trigger `aria-disabled="true"` for the duration of its popin's load (every other tag gets the native `disabled`, which the browser enforces). But the ONLY dispatch route for a pure-legacy `data-gina-popin-name` trigger is the `bindOpen` document click proxy, whose predicate tests the native attribute ALONE — and an `<a>` never carries it here — so a second click during the load reached the open handler and issued a SECOND XHR. `bindDelegatedOpen` returns early for a trigger carrying neither `data-gina-dialog` nor `data-gina-dialog-src`, so `openFromTrigger`'s own `aria-disabled` arm never covered this population. Fix: gate at the top of the legacy per-element open handler, predicate copied VERBATIM from `openFromTrigger` (native arm included, so #B296's `!('disabled' in $el)` rule still holds). **Gating THERE rather than in the proxy is load-bearing, and was MEASURED rather than reasoned:** a trigger's direct children each get their own listener from `proxyClick`, which fires the custom event DIRECTLY and never passes the proxy predicate at all — a proxy-level gate still let the child-click route fire twice, while the handler-level gate stops both (both routes converge there, and `currentTarget` is the element `bindOpen` bound, so child markup cannot dodge it). ⚠️ **Reachability is narrower than it looks, and the scene is easy to get wrong:** a real click HOVERS first, which warms the preload (`warmTrigger` reads `data-gina-popin-url` too), and the click then ADOPTS that in-flight fetch instead of calling `popinLoad` — so nothing is ever armed and the question goes unasked (a probe without the opt-out reads 1 request for a reason unrelated to the gate, its own counter-can-read-2 control failing). The defect needs the documented `data-gina-dialog-preload="false"` opt-out (#B91) — i.e. exactly the triggers whose GET has server-side effects, which is why it is not cosmetic. Whether a no-hover activation (touch, some keyboard paths) is a second reachable population is UNMEASURED. Test `test/e2e/popin-legacy-trigger-aria-disabled.spec.js` (4 arms, red-first; the two fix arms failed 2-vs-1 on the pre-fix dist while their scene/armed/click assertions passed, so the red is attributable to the defect). (12) **A click adopting a still-in-flight hover/focus preload is a genuine wait — it now arms the trigger exactly like the cold click-time XHR (#B285, 0.6.5).** Both consume branches previously armed NOTHING (only a preOpen popin got its skeleton), so with warm-on-intent preloading the COMMON open path showed no busy affordance at all. `consumePreload` gained a guarded 4th param `onSettled`, fired synchronously after the ready (cached) apply and at the TOP of the adopted waiter — success AND failure, release-before-apply, the cold readyState-4 order. The CALLERS own the affordance (arm on adopted-wait only via a settled-flag; release via `releasePopinTrigger`, armPopinTrigger's mirror), keeping `consumePreload` loading-state-agnostic — load-bearing for the extract-and-execute test harness, which injects neither `document` nor `loadingState` and calls with ≤3 args (the guard makes the 4th a no-op), and the right factoring anyway (it reports that it settled; callers decide who cares). The ready branch's SYNCHRONOUS settle is equally load-bearing: without it a caller cannot distinguish ready from in-flight and would arm forever — an instant cached open never flashes a busy state. Double-click protection during the wait comes free from EXISTING mechanisms (the entry gates refuse an armed trigger; the browser suppresses a disabled `<button>`), and the failed-adoption leg releases before the click-time fallback re-arms through popinLoad's own path. Playwright note: an armed `<a>` reads [disabled] in the accessibility tree, so Playwright's own actionability check refuses to click it — a second-click scene needs `force: true` (a real pointer is NOT blocked on aria-disabled). e2e `test/e2e/popin-preload-loading-state.spec.js` (5 arms, red-first: 4 red on the pre-fix dist, ready-branch no-arm control green on both). (13) **A free variable inside a `try` turned every successful JSON load into a fabricated server error (#B315, 0.6.6).** `popinLoad`'s readyState-4 2xx branch dispatched `success.<id>` on `$forms[0]` — but the module's sole `var $forms` is declared in `popinBind`, a SIBLING function under `Popin`, so reading it threw `ReferenceError` straight into that same handler's `catch`, which fabricates `{status: 422, error}` and fires `error.<id>`. `success` therefore never reached a subscriber at all. ⚠️ **The observable is worse than a stack trace, and that is the part worth carrying:** the catch overwrites `result.error` with `JSON.parse(xhr.responseText)` whenever the response is `application/json`, and the throwing line is reachable ONLY for JSON (a non-JSON body returns early at the `loaded.` trigger just above it) — so the consumer received a **422 whose payload was the SUCCESSFUL response body**, reading as a server-side failure that never happened. Fix: dispatch on `$el`, matching the two sibling triggers in the same handler (`loaded.`, `error.`) — `popinLoad` is the CONTENT loader, so a JSON response need not involve a form at all; the `$forms[0]` target was copy-paste from a validator context, whose commented-out `handleXhrResponse` line still sits directly above it. Reproduced BEFORE fixing (a replica of the readyState-4 2xx branch with two firing controls: non-JSON → `loaded`, `location` → early return); tests `test/core/popin.test.js §34` (4 — scope pin, single-declaration-plus-position pin, dispatch-target pin, dist-fidelity pin; both source pins validated RED against the pre-fix source, the dist pin RED before the rebuild). ⚠️ **The registry's other two events still never fire (#B315b):** `progress`'s entire `xhr.onprogress` block including its `triggerEvent` is commented out (dead by construction, with its `ontimeout` sibling), and `click` has ZERO `triggerEvent` sites anywhere — the `popin.click.*` identifiers are per-element DOM event names in a different namespace. Both still register cleanly through `on()`, so the surface advertises three events and delivered none; their disposition is a DESIGN question (implement, or drop them from the registry so the surface stops lying) and was deliberately NOT folded into this fix, which was an unambiguous defect. **The popin `::backdrop` rule is scoped to gina's own dialogs (#B329, fixed 2026-08-24).** The sheet shipped `dialog::backdrop` (dark overlay + blur) UNSCOPED, painting gina's backdrop on EVERY consumer `<dialog>` on the page; now `dialog.gina-popin-container::backdrop`, matching the reduced-motion rules that were already scoped. Both dialog-creation sites set the container class on the `<dialog>` element itself, so gina's own popins keep their backdrop. Consumer-visible: a page relying on the free backdrop for its own dialogs styles them itself now, and a popin dialog element supplied by the page's own markup (the adopt-by-id path) needs `gina-popin-container` to keep gina's backdrop. Pins: `popin-backdrop-scope.test.js` (source + compiled twin + `gina.min.css`, present/absent controls). Browser-bundled ⇒ consumers restart AND re-bake. **A form's or a link's `text/html` XHR answer is routed by the popin the SUBMITTING element is inside, never by "the active popin" (gh#76 + #B571, 2026-09-20).** The validator captures `gina.popin.getPopinContaining($form)` into the per-send closure at submit (the same capture stamps the `X-Gina-Popin-Id`/`-Name` request headers, so `self.isPopinContext()` is true only for a contained form) — and since #B572 (2026-09-22) the STAGED-UPLOAD path makes the same capture once per file selection (`$ownerPopin = getPopinContaining($el.form || $el)`, `$el.form` being the consumer's real form), so the virtual `gina-upload-*` form, its preview lookups, its `uploadProperties.isPopinContext` flag and its staging request belong to the popin the REAL form is inside, or to `document.body`. It was the LAST surviving `isPopinContext()` (« some popin is open ») gate: a PAGE form's file chosen while an unrelated popin was open had its virtual form appended inside that popin — staging 200, all ten hidden metadata fields EMPTY, the save posted without the file, the staging request claiming that popin's id, zero errors — reachable through the file-picker race (the OS picker is application-modal and ignores `inert`, which blocks only the click path) and through script-assigned `input.files`; a form INSIDE the popin was always fine and still is (measured 10/10 both ways). Dev-mode notice `[FormValidator][popin] the staged upload of `#<input>` (form `#<form>`) is placed with its own form …`, from `warnIfOldRulePlacedUpload`, the twin of `warnIfOldRuleRouted`; the popin branch's `$target` DOMParser copy is deliberately untouched (measured working when chosen by containment). Pins: `test/core/validator-upload-popin-containment.test.js` (block-scoped absence pin + the helper driven in a lifted scope with every free identifier supplied), e2e `validator-upload-popin-containment.spec.js` (page form + open popin, the picker race, and the inside-the-popin control, red-first through `B572_PREFIX_BUNDLE`). **A popin close + reopen re-stages, and same-named staged inputs in two forms stay apart (#B733/#B732, 0.7.2):** the file-input change handler reuses a virtual `gina-upload-*` record's element only while it `isConnected`; a detached record is `destroy()`ed (its listeners and their registry entries go with it), deleted from `$forms`, and a new virtual form is built — it used to fall back to `getFormById()`, which returns the validator RECORD, and use that record as the element, so after a popin close (the content is wiped, nothing destroys the virtual form) every selection threw `setAttribute is not a function` and staged nothing. The virtual id of a bracket-less name ends with `-<form id>` (`gina-upload-doc-<formId>`; a bracketed name already carried the form id, once per `]`, and is unchanged) — `gina-upload-doc` was shared by every form, so the second form's upload filled the FIRST form's hidden fields and left its own empty; a listener or selector matching a bracket-less virtual id literally must read the input's `data-gina-form-virtual` instead. Same-named staged inputs inside ONE form still share a virtual form (their hidden-field names collide too). Pins: e2e `validator-upload-popin-reopen-b733.spec.js` (4 arms, incl. #B756's `gina.popin.close(name)`) and `validator-upload-same-name-b732.spec.js` (2 arms), red-first on the pre-fix dist. For a submit, the validator honours its capture at settle only while that popin is still open AND its element still contains the form (a close + reopen in flight detaches the form ⇒ legacy payload); anything else — a page form while a popin is open OR loading — is the legacy handler-only `{contentType, content, status}` payload, with a dev-mode `[FormValidator][popin]` notice naming the popin the old rule would have targeted (it REPLACED an open popin's content, or raised a false `422` `Popin x is not open !` on a loading one — the loading window is reachable through a preload-opted-out trigger, never a hovered default one, whose click adopts the warm). The popin branch now also emits the `.hform` (validator) / `.hlink` (`utils/events.js`, whose `.hform` half is dead — no caller passes `$form`) companions with the parsed xhr-data, so declared callbacks run for forms inside popins (#B571). `getActivePopin()` returns OPEN popins only — the `activePopinId` one when open, else the first open — never a registered-but-not-open one (§8(a)); `popinLoadContent` honours `this` when called as a method (both internal sites use `.call($popin, …)`, the manager-level call keeps the active fallback); the redirect branch resolves `sendCtx.popin || (result.popin ? getActivePopin() : null)` — a plain `location` redirect from a page form falls through to the page redirect (measured pre-fix: it loaded INTO the open popin AND navigated, two fetches), a name-less `{popin:{close}}` with nothing open is a no-op. `getPopinContaining` walks the registry with `getElementById(id).contains(el)` (innermost wins; the element id is the popin id on every flavour; 0.2 µs worst case) — a non-modal popin still inerts the page, so a page form can only be submitted by script while one is open, which is what the request-side arms of `test/e2e/validator-popin-form-target.spec.js` do. Companion `test/e2e/popin-preopen-modeless.spec.js` pins #B574 (`popinOpen` guard `hasAttribute('open')`). Browser-bundled ⇒ consumers restart AND re-bake. **#B575 (0.6.32) — the two hidden transport inputs are DEV-ONLY, and both popin branches now parse tolerantly.** `spliceXhrInputs` runs only under `_isDev` (`NODE_ENV_IS_DEV === 'true'`), so outside dev mode a `renderWithoutLayout` answer carries NO `gina-without-layout-xhr-data`/`-view`; both branches dereferenced `.value` on the absent element, the `TypeError` was caught INSIDE the try and surfaced as a false `422` error callback AFTER a successful server write, and the popin was never loaded. One shared `parseXhrHtmlAnswer()` now returns `{doc, data, view}` with `data`/`view` `null` when absent and the two inputs STRIPPED from the parsed document (they are consumed transport — left in, a swap into a region inside a `<form>` would resubmit them and repeated swaps would duplicate their ids). ⚠️ The parsed data is delivered VERBATIM when present — injecting a `status` key would change the payload every contained form already receives (the slice-1 contract `validator-popin-form-target.spec.js §05` pins) — and the payload is `{status}` only when the inputs are absent; `validator-form-target.test.js §06` pins that absence SCOPED to the popin branch, since `result.status = xhr.status` is legitimate elsewhere (the legacy payload builders in `utils/events.js`). (14) **`data-gina-dialog-target` is ONE selector applied on BOTH sides, with THREE fallbacks — all announced in dev mode since #B580 (2026-09-22).** `applyContent($el, html, $popin, partialTarget)` reads the slot with `$el.querySelector(partialTarget)` and the incoming region with `parsed.querySelector(partialTarget)` over a `new DOMParser()` document, then assigns `$slot.innerHTML = $incoming.innerHTML` — the SLOT ELEMENT survives, which is what preserves chrome and its bindings. The three misses are all fail-soft (a popin open is a retryable read, unlike a submit): a selector the ENGINE REFUSES ⇒ full replace — pre-#B580 this threw an uncaught `SyntaxError` out of the `loaded.<id>` handler / preload-consume tail, so the dialog silently kept its previous content (MEASURED on the real bundle: `Failed to execute 'querySelector' on 'Element': '#slot >' is not a valid selector.`); no match in the OPEN DIALOG ⇒ full replace; no match in the ANSWER ⇒ the whole `parsed.body` into the slot — the SECOND fallback, which the published guide did not mention and nothing pinned. `warnPartialTarget()` emits one `console.warn` per event naming the popin, the selector and which fallback ran, gated on `gina.config.envIsDev`. ⚠️ Testing surface: the e2e harness (`test/e2e/runtime-server.js` serves `envIsDev:'false'`) can assert only the FALLBACK — the NOTICE belongs to `popin.test.js §22`, which drives the REAL brace-walk-extracted function, because the §21 replica omits `$popin` and mirrors the pre-fix body; keep `applyContent`'s signature and first `if ( !partialTarget )` branch byte-shaped or the regex pins at `popin.test.js:1121-1130` and `popin-reload-open.test.js:61` go red. ⚠️ The attribute is the dialog-scoped sibling of `data-gina-form-select`, NOT of `data-gina-form-target`: one selector, both sides, nothing resolved relative to an element, so the `this`/`closest x`/`find x`/`next x` grammar does not apply — all four are valid CSS, so writing one takes the silent fallback instead of reporting a mistake. ⚠️ The "legacy triggers get a full replace" rule covers PURE legacy only: `bindDelegatedOpen` returns early ONLY for a trigger carrying NEITHER `data-gina-dialog` NOR `data-gina-dialog-src`, so a MIXED trigger (legacy attributes + `-src`) routes through `openFromTrigger` and DOES get the partial swap.
946
+ 213. **Client-plugin discipline — CSP-safe listeners, native-dialog parity, preload coalescing (consolidates former #149/#159/#200).** (1) Client-bundle plugins must NOT inject inline event-handler attributes (`setAttribute('onclick', …)`, `el.onX = …`) — they trip CSP `script-src-attr` under nonce-based policies; suppress defaults with `addEventListener('click', e => e.preventDefault())`, and keep it `preventDefault`-ONLY (no stopPropagation) when the element participates in event delegation other handlers depend on — a `preventDefault`-only listener still sets `event.defaultPrevented` exactly as the inline handler did. (2) Dialog popins open as native modals (`$el.showModal()`) in EVERY env — the dev-only non-modal downgrade + manual overlay are gone for dialog mode (native `::backdrop`); a consumer that PRE-OPENS the dialog (skeleton loading) must also use `showModal()` or it is born non-modal with nothing positioning it; the opt-in `preOpen`/`loadingShell` skeleton is idempotent via `hasAttribute('open')` — NOT getAttribute truthiness, because `showModal()` sets `open` to the EMPTY string — and since #B574 (2026-09-20) `popinOpen`'s own re-entry guard reads the same `hasAttribute('open')`: it read getAttribute truthiness, never skipped a shell-opened dialog, and on the new trigger's default MODELESS resolution called `show()` on the still-modal shell, which throws `InvalidStateError` and left `isOpen` false on a visibly open popin; a native UA close (Escape, `<form method="dialog">`) fires the element's `close` event WITHOUT the plugin's own close path, so a once-per-element `close` listener (the event is `close`, not `cancel` — `cancel` is Escape-only) routes it through the plugin close, keeping open-state/listeners/toolbar consistent — a UA-closed shared popin otherwise re-opens "one render behind" (#B58); and since gh#76 §8 (2026-09-20) the `preOpen` loading shell is an explicit STATE — `$popin.isLoading` is true from the moment the shell shows until the real open (`isOpen` stays false, so the two `!isOpen ⇒ popinOpen` consumers still run it), backed by a per-popin load sequence and the in-flight transport (`_loadSeq` / `_loadXhr`): `close()` / `destroy()` on a loading popin cancel the load (`closeLoadingShell` — sequence bump, `abort()`, teardown by the shell's OWN state because the class-keyed teardown never matches a dialog shell, trigger release, `close.<id>`; the aborted transport's readyState-4 handler runs its release block and then drops on the sequence check, so a cancel fires no `error`), the native `close` sync is bound at SHELL time through `bindNativeCloseSync` (guard `isOpen || isLoading`) so an Escape during the load routes through the plugin instead of re-opening when the content lands (driven on the pre-fix bundle: Escape closed the dialog, the landing re-opened it), `loadContent()` on a loading popin injects into the DIALOG element (`target` still names the container until popinOpen re-points it) and completes the open through popinOpen, and a non-2xx load fires `error.<id>` FIRST, then closes the shell if nothing loaded content into it; a popin without `preOpen` never enters the state, and `getActivePopin()` stays open-only; and #B579 (2026-09-20): the declarative trigger's `loaded.<id>` listener applies a NON-STRING detail as nothing — `popinLoadContent`'s redirect branch fires `loaded.<id>` with the POPIN OBJECT as detail (the legacy listener only binds on it), so a `load()` landing on an already-OPEN `data-gina-dialog` popin wrote the new body and then wiped it, leaving the dialog empty with no error (driven on the published bundle; every version since 0.4.6). (3) A popin/dialog click fired TWO identical GETs because the hover/`focusin` preload — `focusin` is part of the click gesture — was never reused: an in-flight preload registry lets the click-time consume ADOPT the in-flight fetch (waiter woken with the body, caller's own load on failure) instead of fetching again, and the legacy click path consumes preloads at all now (#B54); only the initial GET is ever preloaded. The consume path bypassing the full load tail (redirect/JSON handling) was ASSUMED acceptable — but a hover/focus-WARMED trigger whose GET returns a redirect/JSON response (`application/json` `{isXhrRedirect,location}`) had that raw JSON blind-injected as the popin body via `applyContent` (`$el.innerHTML`), the `_self` tunnel never firing — IDENTICAL symptom to the #B77 `_self` timer race but a DIFFERENT mechanism (the consume path, regression `2c61c494` v0.5.5), so #B77 fixed only the UNWARMED click-time path and the WARMED path persisted until #B80 (2026-07-06, `37e6829c`): `preloadFetch`'s `onreadystatechange` now caches a 2xx response ONLY when its Content-Type is NOT `application/json` (mirrors `popinLoad`'s own `isJsonContent` detection), firing the in-flight waiters with `null` (→ `consumePreload`'s `onMiss` = the click-time `doLoad`) and leaving the cache empty (→ a ready consume returns false → `doLoad`), so a JSON-returning trigger falls through to the click-time `popinLoad`'s full redirect/JSON tail exactly as a non-preloaded click (one extra GET on click — the hover-warm GET is not reused; measured 2 GETs). Only a genuine HTML fragment is preloaded+injected. Browser-bundled → prod dist rebuilt; VERIFIED in a real browser (a warmed legacy `data-gina-popin-url` redirect trigger now shows the tunnel target, not the raw JSON; the decline path positively exercised via resource-timing) — jsdom is falsely green. That residual — the GET itself still fired on hover for side-effecting triggers, authenticated (same-origin cookies) and CSRF-token-less (GET) — is CLOSED by the #B91 per-trigger opt-out (2026-07-10): `data-gina-dialog-preload="false"` on the trigger (honored on legacy `data-gina-popin-url` triggers too — the gate reads the attribute off the closest-matched intent target; case-INSENSITIVE `/^false$/i` on purpose, so a templated "False" cannot fail open and fire the GET anyway — deliberately safer than data-gina-dialog-modal's case-sensitive parse) suppresses BOTH the hover warm and the focusin-on-click warm at the shared intent handler; the opted-out click loads normally at click time (undefined cache slot → consume returns false → the caller's click-time load; exactly one GET per click). Default stays preload-ON: GET is presumed safe to fire early per HTTP semantics — a side-effecting trigger declares itself (the hover-prefetch ecosystem's convention, live-verified). (4) A proxied/tunnelled redirect JSON response (`isXhrRedirect`+`location`, default `_self`) used to `$popin.load()` then arm a blind `setTimeout(50, () => !$popin.isOpen && $popin.open())`; a follow-up load slower than 50 ms opened the popin against a not-yet-injected (skeleton/empty) target → intermittent unhandled-deref crash. Removed as vestigial (v0.1.0, 2021) — the already-armed `loaded.<id>` listener opens CONTENT-FIRST (injects the body via popinBind/handleLoadedBody, THEN popinOpen), so the load alone suffices; the `_self` branch's `return;` still guards the `window.open` fall-through (non-`_self` targets only) (#B77). Event contract: `open.<id>` = a fresh open, `loaded.<id>` = reload/redirect content injected — a consumer needing "content ready on every load" must listen to BOTH. A structurally identical blind timer lived in the SIBLING validator `Validator::Popin now redirecting` path (`core/plugins/lib/validator/src/main.js`), removed by #B79 (2026-07-10): the validator DISCARDED popinLoad's return, and that return IS the `{open}` handle whose `open()` arms the content-first `loaded.<id>` listener — so the timer was papering over a race the discarded handle created (an XHR faster than 50 ms fired `loaded` into the void and the body was lost, a slower one opened an empty popin first). It now captures the handle and calls `open()` inside the same not-open gate, guarding the `undefined` popinLoad returns when the request cannot start (CORS unsupported) so a failed load never blind-opens an empty popin; and the cross-popin branch resets `isRedirecting` before closing the ORIGINAL popin, because `popinClose` early-bails on a redirecting popin — that close was a silent no-op, leaving the original open behind the new one (browser-verified: pre-fix stays open, post-fix closes). MEASURED CORRECTION: the timer was NOT load-bearing for the different-popin branch as long assumed — that branch threw first, because the published `gina.popin` WAS re-assembled with a target-wins merge on every `new Popin()`, so its `getPopinByName`/`getPopinById` stayed bound to the FIRST instance's registry and resolved ONLY the boot popin, while `$popins` + `getActivePopin` saw every popin and `gina.popin.activePopinId` never updated (so `getActivePopin()` returned null once nothing was open) — and popins registered AFTER an instance's publish (click-time in-page dialog registrations) never reached the published registry at all, with destroyed popins lingering in it. FIXED as #B90 (2026-07-10): ONE module-scoped registry (`_sharedPopins`) aliased by every instance's `$popins`; a publish-ONCE `gina.popin = instance` (the published object IS the first instance, LIVE — re-publishing was both the freeze defect and, with a shared registry, a self-merge recursion hazard: gina's merge deep-recurses plain objects and `$popin.eventData` can hold the $popin itself); and a `setActivePopinId` write-through helper at all 7 write sites keeping the published `activePopinId` truthful no matter which instance opens or closes a popin. Browser-verified both ways on the built bundle (pre-fix: accessors blind + the validator cross-popin redirect 422-throws `not found`; fixed: accessors resolve every instance's popins and a form submit redirecting into a DIFFERENT popin works end-to-end — the original closes, the target loads content-first, `activePopinId` follows). Generalises: an accessor published through a target-wins `merge()` silently keeps the first instance's closure — publish a live instance once and share module-scoped state instead of re-merging per construction; and never infer a code path is reachable from the fact that it exists. (5) EAGER content warm — `data-gina-dialog-preload="eager"` (case-insensitive) opts a trigger into a one-shot idle warm-all pass: after `window` load, on requestIdleCallback (setTimeout fallback), serialized one GET at a time, routed through the SAME shared per-trigger gate as the hover warm (`warmTrigger`: disabled skip → the `"false"` opt-out → URL-cache dedup → in-flight reserve + fetch) so the two warm paths cannot drift and whichever fires second is a no-op; skipped entirely under Save-Data; triggers injected after the pass keep the delegated hover warm; staleness matches the shipped no-TTL hover semantics — the opt-in accepts the wider warm→open window. (6) #B139 (2026-07-20, 0.5.22): the content cache no longer outlives the open it warmed — measured pre-fix it was a ONE-GENERATION-LAGGING store (every open after the first paid ~1 GET yet rendered the PREVIOUS open's fetch: the leftover entry — the #B54 in-flight adoption's repopulated body, or the around-open re-warm — blocked the fresh warm's dedup; NO invalidation path existed, close/unbind/destroy never touched the cache; reproduced back to 0.5.4, so it predates #B54 and the eager pass). Fix: close clears the popin's content-URL slot (stamped at consumePreload both branches + popinLoad) AND re-sweeps the same slot ~120ms later, because the close-time a11y focus-return and pointer re-hover fire TRUSTED synthetic intents that re-warm the slot with close-era content within 1ms of the close (measured; a raced sweep is benign by construction — at most one extra fetch, never staleness, since an adopted in-flight body still reaches its open through the waiter chain). AND `preload="false"` triggers now skip the cache READ at open on both open paths — `false` is the hard always-refetch spelling for volatile popins (its GET always happens at open, never from a same-URL sibling's warm); no new annotation vocabulary. Warmed-never-opened entries keep the page-lifetime eager semantics above; default triggers pay at most one extra idempotent GET per close (the swept synthetic warm). Content preload deliberately does NOT ride 103 Early Hints: popin responses vary on `X-Requested-With` (a non-XHR GET of a fragment layout gets the iframe-wrap variant and misses the scripts append), and a browser `Link rel=preload` fetch never carries that header — wrong-variant bytes or a double GET, with no success branch; `self.setEarlyHints()` remains the right tool for a popin's STATIC subassets (the CSS/JS it injects via getScript/getStyle). (7) #A11Y8 (2026-08-05, 0.6.4): the background-`inert` loop in `applyNonModalShims` skipped `instance.target` — the shared `.gina-popins` container — and since every popin lives INSIDE that container, and `popinOpen` never closes the popin it supersedes (it only overwrites `activePopinId`), opening a second NON-MODAL dialog left the first one fully keyboard-reachable behind it: a user could tab out of the dialog on screen and into a stale one. Non-modal is the FRAMEWORK DEFAULT for `data-gina-dialog` (`resolveModal` falls through to `return false` at step 5; only a legacy `data-gina-popin-name` trigger is unconditionally modal), so this was the ordinary path, not an exotic one — note item (2)'s "native modals in EVERY env" predates the non-modal branch and now describes LEGACY triggers only. Fix: descend into the container instead of skipping it, and inert sibling `dialog[open]` ONLY — a `<dialog>` without `open` is already `display:none` per the UA stylesheet, so it is unreachable without help (measured in Chrome with controls firing both directions; the modal path was never affected, native `showModal()` handles the top layer itself). Teardown needed NO change: `removeNonModalShims` sweeps `[data-gina-popin-inert]` document-wide, so dialogs nested in the container restore without it knowing they exist. The `getAttribute('inert') == null` guard means gina only marks what it set, so a consumer-set `inert` is never stamped and never stripped. All seven are browser-bundled — prod dist rebuild required; verify (2), (3), (4) and (5) with a REAL preemptive-open / preload / redirect-tunnel consumer — a minimal smoke page is falsely green; (7) needs TWO popins open at once, the only scene in which it differs at all. (8) **A trigger gate must not trust the native `disabled` ATTRIBUTE on an element the browser already enforces (#B296, 0.6.5).** The three DISPATCH-time gates (`openFromTrigger`, the `bindOpen` document proxy, the per-`$element` listener) now require `!('disabled' in $el)` before the attribute counts, mirroring the validator's `isTriggerDisabled` (#B293) so the two subsystems agree on what "disabled" means. Rationale, measured: a natively-disabled `<button>` delivers **0 clicks to JS**, so the attribute arm could only ever fire on a `disabled` written DURING the dispatch — the shape of every consumer double-submit guard — which ate the open and then cleared the attribute, leaving nothing marked and a normal-looking control; on an `<a>`/custom element/span the browser enforces nothing, so the arm is KEPT (`'disabled' in $el` → `button:true · input:true · anchor:false · custom-element:false · span:false`). It also cannot weaken gina's OWN re-entry guard: `armPopinTrigger` arms `<a>` with `aria-disabled` (untouched arm) and every other tag with native `disabled` — enforced by the browser for real controls, and still enforced by this gate for `<span>`-likes, which is exactly the branch the `in` test preserves. **Two gates are deliberately NOT changed:** the hover/focus `warmTrigger` preload gate (removing it there would start PRELOADING a `<button disabled>` nobody can click — if it ever changes it wants the IDL property `$el.disabled === true`, not the ignore treatment) and the bind-time `$link` skip (structurally unreachable by a mid-dispatch write, and it carries a different one-arm predicate). **Reachability is unequal and the difference is load-bearing:** at `openFromTrigger` gina's proxy is DELEGATED so a button-bound guard always wins the race (plain #B293 shape reproduces); at the per-`$element` listener gina binds to the NODE, so only a CAPTURE-phase or earlier-registered guard can poison it (measured both ways — a target-phase guard bound after gina's does NOT reproduce); the legacy document-proxy site is traced-not-exercised. Test `test/e2e/popin-trigger-native-disabled.spec.js` — a replica cannot catch this, it is an ordering interaction between a consumer listener and a delegated proxy. ⚠️ **Scene trap for anyone writing a popin close-button test:** the close element must carry NO `id` — gina assigns `popin.close.<n>` only to an id-less one and the dispatch gate matches on that prefix, so a consumer id makes EVERY arm read "did not close", subtract control included (that is its own defect, #B299). (9) **A teardown loop must never splice the array it is walking (#B265, 0.6.5).** `popinUnbind`'s validator-form teardown spliced `$popin['$forms']` inside a `for` bounded by a length captured BEFORE the loop, so each splice shifted the array left under an incrementing cursor and the read index skipped one every time — the originally ODD-indexed forms were never destroyed and were left behind in the very array the loop exists to empty. Because the popin's `innerHTML = ''` runs BEFORE the loop, their surviving validator entries pointed at already-detached nodes, and `validateFormById`'s "return existing when available" early return then handed the stale entry back on the next open, so the form and its submit trigger came up silently inert — the same end symptom as #B294 by a completely different route. Fix: snapshot with `.slice()`, clear once with `.length = 0`, iterate the snapshot. **Two things worth carrying beyond this bug.** First, `destroy()` is not free: it fires `destroy.<formId>` (validator `main.js:4241`) and `'destroy'` IS on the plugin's public registrable-event list, so making a skipped form destroy properly starts firing consumer `on('destroy')` callbacks that never fired before — a teardown fix is a behaviour change, not merely a leak fix. Second, per-form teardown is now wrapped: `destroy()` → `unbindForm` → `getFormById(vFormId)` THROWS for a form holding a `data-gina-form-virtual` file input once its virtual form is unresolvable, which is precisely the detached state every form is in at that point — and before the guard, one such throw silently abandoned every LATER form in the loop. **Reachability is much narrower than it looks and must not be overstated:** the loop is gated on a validator — since #B756 (0.7.2) the one the POPIN was registered with (`$popin.options.validator`, falling back to the closure's `$validatorInstance`); before, the closure alone, a PER-INSTANCE value assigned only from `options.validator`. Gina's own boot does `new Popin({name:'gina-dialog-boot'})` with no validator, and the delegated `data-gina-dialog` listener is installed once by that boot instance (module guard `_ginaDialogDelegated`), so **the declarative dialog path can never reach this code** — it needs a popin constructed with a validator (`new Popin({name, validator})`, the legacy popin API). #B756 was the closure gate's own defect: `gina.popin` IS that validator-less boot instance, so the documented `gina.popin.close(name)` skipped a validator-registered popin's teardown and the reopened form came up unbound (`validateFormById`'s early return again — a third route to the #B265 symptom), while the popin's own `close()` and its close button run the registering instance and always tore down. A client navigation took the same route: gina/nav's fragment swap closes the active popin through `gina.popin.close(name)` (`closeActivePopin`), so a validator popin still open when a navigation swapped the page skipped the teardown too (measured on pre-0.7.2 bundles). Since #B762 (0.7.3) that close runs BEFORE the swap moves focus to the region and applies its scroll decision: run after them, closing the popin returned focus to its trigger and scrolled the page back down to it, modal and non-modal popins alike (an open popin inerts the page outside it, so the region could not take focus); e2e `nav-popin-close-focus-b762.spec.js`. Pins: arm 04 of e2e `validator-upload-popin-reopen-b733.spec.js`, and e2e `validator-upload-popin-reopen-nav-b756.spec.js` for the navigation. That is also why the test is a replica + source pins (`test/core/popin-forms-teardown.test.js`) rather than an e2e arm: an e2e scene on the declarative path would be VOID, every arm reading "nothing destroyed" for a reason unrelated to the defect. (10) **Never re-derive an element's ROLE from its id when the call path already proves it (#B299 + #B301, 0.6.5).** `register()`'s close branch gated on `/^popin\.close\./.test(event.target.id)` — a prefix gina mints at bind time — and that single line carried TWO silent defects: gina assigns the prefixed id **only to an id-less element** (`:1560`), so a consumer-supplied id is taken verbatim and matches nothing (**#B299**); and `event.target` is whatever was actually CLICKED, so an icon nested inside the button matches nothing either (**#B301** — the ordinary `<button class="gina-popin-close"><svg/></button>`, and by far the wider case). Both are invisible: `cancelEvent` runs at the top of the listener, so the button also swallows its own default — no error, no navigation, nothing. Fix: read **`event.currentTarget`** (the element `register()` bound — a `.gina-popin-close` by construction, since that function has ONE call site fed only from `querySelectorAll('.gina-popin-close')`) and treat a `popin.click.*` id as the sole exception, which preserves the dual-role element (a trigger that is ALSO a close button — reachable because `bindOpen` scans `document`, not just the host page). ⚠️ **What must NOT be "fixed" instead: rewriting the element's id.** The `$close` teardown sweep removes this listener via `gina.events[eId] == eId` — the degenerate name===id entry that `events.js:42` produces — so decoupling the event name from the id leaks the listener; and overwriting a consumer id would break their selectors, `aria-labelledby` and test hooks. Contrast `bindOpen:1195-1203`, which takes the OPPOSITE policy and does clobber a consumer id on trigger elements. Measured with controls firing both directions on the real bundle (id-less closes / consumer-id does not / nested click does not / patched all close), test `test/e2e/popin-close-dispatch.spec.js`. (11) **gina's own in-flight arm must be honoured by the path that dispatches it — a legacy trigger re-fired during its own load (#B298, 0.6.5).** `armPopinTrigger` marks an `<a>` trigger `aria-disabled="true"` for the duration of its popin's load (every other tag gets the native `disabled`, which the browser enforces). But the ONLY dispatch route for a pure-legacy `data-gina-popin-name` trigger is the `bindOpen` document click proxy, whose predicate tests the native attribute ALONE — and an `<a>` never carries it here — so a second click during the load reached the open handler and issued a SECOND XHR. `bindDelegatedOpen` returns early for a trigger carrying neither `data-gina-dialog` nor `data-gina-dialog-src`, so `openFromTrigger`'s own `aria-disabled` arm never covered this population. Fix: gate at the top of the legacy per-element open handler, predicate copied VERBATIM from `openFromTrigger` (native arm included, so #B296's `!('disabled' in $el)` rule still holds). **Gating THERE rather than in the proxy is load-bearing, and was MEASURED rather than reasoned:** a trigger's direct children each get their own listener from `proxyClick`, which fires the custom event DIRECTLY and never passes the proxy predicate at all — a proxy-level gate still let the child-click route fire twice, while the handler-level gate stops both (both routes converge there, and `currentTarget` is the element `bindOpen` bound, so child markup cannot dodge it). ⚠️ **Reachability is narrower than it looks, and the scene is easy to get wrong:** a real click HOVERS first, which warms the preload (`warmTrigger` reads `data-gina-popin-url` too), and the click then ADOPTS that in-flight fetch instead of calling `popinLoad` — so nothing is ever armed and the question goes unasked (a probe without the opt-out reads 1 request for a reason unrelated to the gate, its own counter-can-read-2 control failing). The defect needs the documented `data-gina-dialog-preload="false"` opt-out (#B91) — i.e. exactly the triggers whose GET has server-side effects, which is why it is not cosmetic. Whether a no-hover activation (touch, some keyboard paths) is a second reachable population is UNMEASURED. Test `test/e2e/popin-legacy-trigger-aria-disabled.spec.js` (4 arms, red-first; the two fix arms failed 2-vs-1 on the pre-fix dist while their scene/armed/click assertions passed, so the red is attributable to the defect). (12) **A click adopting a still-in-flight hover/focus preload is a genuine wait — it now arms the trigger exactly like the cold click-time XHR (#B285, 0.6.5).** Both consume branches previously armed NOTHING (only a preOpen popin got its skeleton), so with warm-on-intent preloading the COMMON open path showed no busy affordance at all. `consumePreload` gained a guarded 4th param `onSettled`, fired synchronously after the ready (cached) apply and at the TOP of the adopted waiter — success AND failure, release-before-apply, the cold readyState-4 order. The CALLERS own the affordance (arm on adopted-wait only via a settled-flag; release via `releasePopinTrigger`, armPopinTrigger's mirror), keeping `consumePreload` loading-state-agnostic — load-bearing for the extract-and-execute test harness, which injects neither `document` nor `loadingState` and calls with ≤3 args (the guard makes the 4th a no-op), and the right factoring anyway (it reports that it settled; callers decide who cares). The ready branch's SYNCHRONOUS settle is equally load-bearing: without it a caller cannot distinguish ready from in-flight and would arm forever — an instant cached open never flashes a busy state. Double-click protection during the wait comes free from EXISTING mechanisms (the entry gates refuse an armed trigger; the browser suppresses a disabled `<button>`), and the failed-adoption leg releases before the click-time fallback re-arms through popinLoad's own path. Playwright note: an armed `<a>` reads [disabled] in the accessibility tree, so Playwright's own actionability check refuses to click it — a second-click scene needs `force: true` (a real pointer is NOT blocked on aria-disabled). e2e `test/e2e/popin-preload-loading-state.spec.js` (5 arms, red-first: 4 red on the pre-fix dist, ready-branch no-arm control green on both). (13) **A free variable inside a `try` turned every successful JSON load into a fabricated server error (#B315, 0.6.6).** `popinLoad`'s readyState-4 2xx branch dispatched `success.<id>` on `$forms[0]` — but the module's sole `var $forms` is declared in `popinBind`, a SIBLING function under `Popin`, so reading it threw `ReferenceError` straight into that same handler's `catch`, which fabricates `{status: 422, error}` and fires `error.<id>`. `success` therefore never reached a subscriber at all. ⚠️ **The observable is worse than a stack trace, and that is the part worth carrying:** the catch overwrites `result.error` with `JSON.parse(xhr.responseText)` whenever the response is `application/json`, and the throwing line is reachable ONLY for JSON (a non-JSON body returns early at the `loaded.` trigger just above it) — so the consumer received a **422 whose payload was the SUCCESSFUL response body**, reading as a server-side failure that never happened. Fix: dispatch on `$el`, matching the two sibling triggers in the same handler (`loaded.`, `error.`) — `popinLoad` is the CONTENT loader, so a JSON response need not involve a form at all; the `$forms[0]` target was copy-paste from a validator context, whose commented-out `handleXhrResponse` line still sits directly above it. Reproduced BEFORE fixing (a replica of the readyState-4 2xx branch with two firing controls: non-JSON → `loaded`, `location` → early return); tests `test/core/popin.test.js §34` (4 — scope pin, single-declaration-plus-position pin, dispatch-target pin, dist-fidelity pin; both source pins validated RED against the pre-fix source, the dist pin RED before the rebuild). ⚠️ **The registry's other two events still never fire (#B315b):** `progress`'s entire `xhr.onprogress` block including its `triggerEvent` is commented out (dead by construction, with its `ontimeout` sibling), and `click` has ZERO `triggerEvent` sites anywhere — the `popin.click.*` identifiers are per-element DOM event names in a different namespace. Both still register cleanly through `on()`, so the surface advertises three events and delivered none; their disposition is a DESIGN question (implement, or drop them from the registry so the surface stops lying) and was deliberately NOT folded into this fix, which was an unambiguous defect. **The popin `::backdrop` rule is scoped to gina's own dialogs (#B329, fixed 2026-08-24).** The sheet shipped `dialog::backdrop` (dark overlay + blur) UNSCOPED, painting gina's backdrop on EVERY consumer `<dialog>` on the page; now `dialog.gina-popin-container::backdrop`, matching the reduced-motion rules that were already scoped. Both dialog-creation sites set the container class on the `<dialog>` element itself, so gina's own popins keep their backdrop. Consumer-visible: a page relying on the free backdrop for its own dialogs styles them itself now, and a popin dialog element supplied by the page's own markup (the adopt-by-id path) needs `gina-popin-container` to keep gina's backdrop. Pins: `popin-backdrop-scope.test.js` (source + compiled twin + `gina.min.css`, present/absent controls). Browser-bundled ⇒ consumers restart AND re-bake. **A form's or a link's `text/html` XHR answer is routed by the popin the SUBMITTING element is inside, never by "the active popin" (gh#76 + #B571, 2026-09-20).** The validator captures `gina.popin.getPopinContaining($form)` into the per-send closure at submit (the same capture stamps the `X-Gina-Popin-Id`/`-Name` request headers, so `self.isPopinContext()` is true only for a contained form) — and since #B572 (2026-09-22) the STAGED-UPLOAD path makes the same capture once per file selection (`$ownerPopin = getPopinContaining($el.form || $el)`, `$el.form` being the consumer's real form), so the virtual `gina-upload-*` form, its preview lookups, its `uploadProperties.isPopinContext` flag and its staging request belong to the popin the REAL form is inside, or to `document.body`. It was the LAST surviving `isPopinContext()` (« some popin is open ») gate: a PAGE form's file chosen while an unrelated popin was open had its virtual form appended inside that popin — staging 200, all ten hidden metadata fields EMPTY, the save posted without the file, the staging request claiming that popin's id, zero errors — reachable through the file-picker race (the OS picker is application-modal and ignores `inert`, which blocks only the click path) and through script-assigned `input.files`; a form INSIDE the popin was always fine and still is (measured 10/10 both ways). Dev-mode notice `[FormValidator][popin] the staged upload of `#<input>` (form `#<form>`) is placed with its own form …`, from `warnIfOldRulePlacedUpload`, the twin of `warnIfOldRuleRouted`; the popin branch's `$target` DOMParser copy is deliberately untouched (measured working when chosen by containment). Pins: `test/core/validator-upload-popin-containment.test.js` (block-scoped absence pin + the helper driven in a lifted scope with every free identifier supplied), e2e `validator-upload-popin-containment.spec.js` (page form + open popin, the picker race, and the inside-the-popin control, red-first through `B572_PREFIX_BUNDLE`). **A popin close + reopen re-stages, and same-named staged inputs in two forms stay apart (#B733/#B732, 0.7.2):** the file-input change handler reuses a virtual `gina-upload-*` record's element only while it `isConnected`; a detached record is `destroy()`ed (its listeners and their registry entries go with it), deleted from `$forms`, and a new virtual form is built — it used to fall back to `getFormById()`, which returns the validator RECORD, and use that record as the element, so after a popin close (the content is wiped, nothing destroys the virtual form) every selection threw `setAttribute is not a function` and staged nothing. The virtual id of a bracket-less name ends with `-<form id>` (`gina-upload-doc-<formId>`; a bracketed name already carried the form id, once per `]`, and is unchanged) — `gina-upload-doc` was shared by every form, so the second form's upload filled the FIRST form's hidden fields and left its own empty; a listener or selector matching a bracket-less virtual id literally must read the input's `data-gina-form-virtual` instead. Same-named staged inputs inside ONE form still share a virtual form (their hidden-field names collide too). Pins: e2e `validator-upload-popin-reopen-b733.spec.js` (4 arms, incl. #B756's `gina.popin.close(name)`) and `validator-upload-same-name-b732.spec.js` (2 arms), red-first on the pre-fix dist. For a submit, the validator honours its capture at settle only while that popin is still open AND its element still contains the form (a close + reopen in flight detaches the form ⇒ legacy payload); anything else — a page form while a popin is open OR loading — is the legacy handler-only `{contentType, content, status}` payload, with a dev-mode `[FormValidator][popin]` notice naming the popin the old rule would have targeted (it REPLACED an open popin's content, or raised a false `422` `Popin x is not open !` on a loading one — the loading window is reachable through a preload-opted-out trigger, never a hovered default one, whose click adopts the warm). The popin branch now also emits the `.hform` (validator) / `.hlink` (`utils/events.js`, whose `.hform` half is dead — no caller passes `$form`) companions with the parsed xhr-data, so declared callbacks run for forms inside popins (#B571). `getActivePopin()` returns OPEN popins only — the `activePopinId` one when open, else the first open — never a registered-but-not-open one (§8(a)); `popinLoadContent` honours `this` when called as a method (both internal sites use `.call($popin, …)`, the manager-level call keeps the active fallback); the redirect branch resolves `sendCtx.popin || (result.popin ? getActivePopin() : null)` — a plain `location` redirect from a page form falls through to the page redirect (measured pre-fix: it loaded INTO the open popin AND navigated, two fetches), a name-less `{popin:{close}}` with nothing open is a no-op. `getPopinContaining` walks the registry with `getElementById(id).contains(el)` (innermost wins; the element id is the popin id on every flavour; 0.2 µs worst case) — a non-modal popin still inerts the page, so a page form can only be submitted by script while one is open, which is what the request-side arms of `test/e2e/validator-popin-form-target.spec.js` do. Companion `test/e2e/popin-preopen-modeless.spec.js` pins #B574 (`popinOpen` guard `hasAttribute('open')`). Browser-bundled ⇒ consumers restart AND re-bake. **#B575 (0.6.32) — the two hidden transport inputs are DEV-ONLY, and both popin branches now parse tolerantly.** `spliceXhrInputs` runs only under `_isDev` (`NODE_ENV_IS_DEV === 'true'`), so outside dev mode a `renderWithoutLayout` answer carries NO `gina-without-layout-xhr-data`/`-view`; both branches dereferenced `.value` on the absent element, the `TypeError` was caught INSIDE the try and surfaced as a false `422` error callback AFTER a successful server write, and the popin was never loaded. One shared `parseXhrHtmlAnswer()` now returns `{doc, data, view}` with `data`/`view` `null` when absent and the two inputs STRIPPED from the parsed document (they are consumed transport — left in, a swap into a region inside a `<form>` would resubmit them and repeated swaps would duplicate their ids). ⚠️ The parsed data is delivered VERBATIM when present — injecting a `status` key would change the payload every contained form already receives (the slice-1 contract `validator-popin-form-target.spec.js §05` pins) — and the payload is `{status}` only when the inputs are absent; `validator-form-target.test.js §06` pins that absence SCOPED to the popin branch, since `result.status = xhr.status` is legitimate elsewhere (the legacy payload builders in `utils/events.js`). (14) **`data-gina-dialog-target` is ONE selector applied on BOTH sides, with THREE fallbacks — all announced in dev mode since #B580 (2026-09-22).** `applyContent($el, html, $popin, partialTarget)` reads the slot with `$el.querySelector(partialTarget)` and the incoming region with `parsed.querySelector(partialTarget)` over a `new DOMParser()` document, then assigns `$slot.innerHTML = $incoming.innerHTML` — the SLOT ELEMENT survives, which is what preserves chrome and its bindings. The three misses are all fail-soft (a popin open is a retryable read, unlike a submit): a selector the ENGINE REFUSES ⇒ full replace — pre-#B580 this threw an uncaught `SyntaxError` out of the `loaded.<id>` handler / preload-consume tail, so the dialog silently kept its previous content (MEASURED on the real bundle: `Failed to execute 'querySelector' on 'Element': '#slot >' is not a valid selector.`); no match in the OPEN DIALOG ⇒ full replace; no match in the ANSWER ⇒ the whole `parsed.body` into the slot — the SECOND fallback, which the published guide did not mention and nothing pinned. `warnPartialTarget()` emits one `console.warn` per event naming the popin, the selector and which fallback ran, gated on `gina.config.envIsDev`. ⚠️ Testing surface: the e2e harness (`test/e2e/runtime-server.js` serves `envIsDev:'false'`) can assert only the FALLBACK — the NOTICE belongs to `popin.test.js §22`, which drives the REAL brace-walk-extracted function, because the §21 replica omits `$popin` and mirrors the pre-fix body; keep `applyContent`'s signature and first `if ( !partialTarget )` branch byte-shaped or the regex pins at `popin.test.js:1121-1130` and `popin-reload-open.test.js:61` go red. ⚠️ The attribute is the dialog-scoped sibling of `data-gina-form-select`, NOT of `data-gina-form-target`: one selector, both sides, nothing resolved relative to an element, so the `this`/`closest x`/`find x`/`next x` grammar does not apply — all four are valid CSS, so writing one takes the silent fallback instead of reporting a mistake. ⚠️ The "legacy triggers get a full replace" rule covers PURE legacy only: `bindDelegatedOpen` returns early ONLY for a trigger carrying NEITHER `data-gina-dialog` NOR `data-gina-dialog-src`, so a MIXED trigger (legacy attributes + `-src`) routes through `openFromTrigger` and DOES get the partial swap.
947
947
 
948
948
  214. **Per-request routing-clone correctness — narrow the defensive clone, and propagate mutations onto returned clones (consolidates former #194/#195; #B52-residual, 2026-06-18).** The router's per-request `options.conf = JSON.clone(conf)` — a multi-MB deep clone of the ENTIRE bundle config per matched request, held for the request's whole query+render window — was narrowed to: shallow-copy the top level + `conf.content`, deep-clone ONLY `conf.content.routing`, the one subtree actually MUTATED per request (audited: every other per-request write is a whole-subtree REASSIGNMENT a shallow copy isolates, and plugins/connectors/models read config via their own clones, never through the request's) — removing a dev/high-load heap high-water-mark proportional to config size (measured 163→62 MB peak under 150 concurrent slow requests, drains on idle). **Narrowed again — the routing deep clone is retired too (#P40 S3b, 2026-09-09):** the only per-request write on that subtree is a whole-property REPLACEMENT on the MATCHED rule (`options.conf.content.routing[options.rule].param = params.param`), and `params.middleware` — the array `processMiddlewares` splices — is already the request's own copy from the matcher; so `this.route` now shallow-copies the routing map and shallow-copies the matched rule only, every other rule staying shared by reference (the deep clone cost ~5 µs at 8 routes and ~290 µs at 380 on the dev host, on every matched request, for a map nothing else mutates). Pinned by `test/core/router-per-rule-copy.test.js` (source pins with a comment-strip control + a behavioural compiled from the shipped source whose shared-rule arm discriminates against the deep clone) and the realigned `test/core/router.test.js §11`. Rule: copy only what a per-request write actually touches, at the granularity of the write — a whole-property replacement needs a shallow copy of its parent, never a deep clone of the tree — and share the immutable remainder by reference; but audit EVERY per-request writer first, or a missed mutate-into-a-shared-subtree reintroduces cross-request contamination. Sibling: `getRouteByUrl` (server-side redirect/relative-route resolution AND browser-bundled) aliased the shared route config's `param` by REFERENCE while the matcher rewrites `param.{path,namespace,file,title}` in place for `:placeholder` routes — request A's substitution contaminated request B (server) and repeat navigations (client). The fix is TWO coordinated edits, not just "clone the alias": clone `param` per request AND propagate the substituted param onto the returned route object — the function returns a fresh clone of the SINGLETON, so cloning alone returns the un-substituted `:placeholder` and regresses even the first request. Rule: when a matcher both aliases a shared config object and returns a re-clone of that object, isolating the alias is insufficient — propagate the per-request mutations onto the returned clone. The sibling is browser-bundled → prod dist rebuild. **Third member of the same family — do not COPY a key the source does not have (#B423, 2026-08-26).** The `setOptions` per-request loop that refreshes each cloned rule's `host`/`hostname` from `envConf.routing[r]` copied both unconditionally, for EVERY rule. But `config.js`'s host/hostname defaulting deliberately EXCLUDES `param.control === "redirect"` rules (`… && !/^redirect$/.test(routing[rule].param.control)`), so those keys are **ABSENT** there — and `target.k = source.k` on an absent `source.k` CREATES an own property valued `undefined`. The write lands on `envConf._routingCloned`, which is cached for the PROCESS LIFETIME, so the poisoning is permanent: every later no-arg `getConfig()` → `JSON.clone(local.options.conf)` then hit `source[key] === undefined` and emitted the clone's `"should not be left \`undefined\`. Assigning to \`null\`"` warn — one `host` + one `hostname` per redirect rule per clone, forever (a consumer measured it as ~76% of that log line's volume, drowning the genuine warns it exists to surface). Fixed by existence-guarding both copies (`typeof(source.k) != 'undefined'`), so absent stays absent. **Two gates bound the surface, both measured:** the whole block sits inside `if ( typeof(local.options.template) != 'undefined' && … )` — TEMPLATE/view routes ONLY, so an API-only bundle never reproduced it — and the copy loop is `_isRoutingUpdateNeeded`-gated. **Rule: in a refresh-the-clone loop, an assignment is not free — copying an absent key MANUFACTURES an `undefined` the clone utility is right to complain about. Guard the copy, never the warn** (silencing `JSON.clone`'s warn would hide a signal the framework deliberately emits), and never "fix" it upstream by giving redirect rules a host — that exclusion is intentional. Server-side only (controller.js is not browser-bundled — verified by dist token grep with a firing control) ⇒ restart, no re-bake. Tests: `test/core/controller-routing-host-copy.test.js` (source pins on the guard + the upstream exclusion, plus a real-`JSON.clone` behavioral whose SUBTRACT arm runs the pre-fix unguarded shape and asserts it both creates the keys and fires exactly one warn per key per rule).
949
949