@honua/app-platform 0.1.2-beta.0 → 0.1.4-beta.0

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 (383) hide show
  1. package/collaboration/client.d.ts.map +1 -1
  2. package/connect-url-safety.d.ts +53 -0
  3. package/connect-url-safety.js +189 -0
  4. package/contract/discovery.d.ts +34 -0
  5. package/contract/discovery.d.ts.map +1 -1
  6. package/contract/index.d.ts +7 -5
  7. package/contract/index.d.ts.map +1 -1
  8. package/contract/index.js +3 -2
  9. package/contract/pmtiles.d.ts +10 -1
  10. package/contract/pmtiles.d.ts.map +1 -1
  11. package/contract/pmtiles.js +10 -2
  12. package/contract/protocol-module.d.ts +84 -9
  13. package/contract/protocol-module.d.ts.map +1 -1
  14. package/contract/query-filter-fes.d.ts +18 -0
  15. package/contract/query-filter-fes.d.ts.map +1 -0
  16. package/contract/query-filter-fes.js +122 -0
  17. package/contract/query-filter.d.ts +300 -0
  18. package/contract/query-filter.d.ts.map +1 -0
  19. package/contract/query-filter.js +883 -0
  20. package/contract/schema.d.ts +12 -0
  21. package/contract/schema.d.ts.map +1 -1
  22. package/contract/schema.js +34 -0
  23. package/contract/source-capability-support.d.ts.map +1 -1
  24. package/contract/source-capability-support.js +15 -3
  25. package/contract/source.d.ts +45 -25
  26. package/contract/source.d.ts.map +1 -1
  27. package/contract/source.js +719 -479
  28. package/contract/types.d.ts +87 -18
  29. package/contract/types.d.ts.map +1 -1
  30. package/contract/widget-source.d.ts.map +1 -1
  31. package/controls/basemap-switcher.d.ts +19 -11
  32. package/controls/basemap-switcher.d.ts.map +1 -1
  33. package/controls/basemap-switcher.js +58 -25
  34. package/controls/catalog.d.ts +105 -0
  35. package/controls/catalog.d.ts.map +1 -0
  36. package/controls/catalog.js +341 -0
  37. package/controls/element-utils.d.ts +9 -0
  38. package/controls/element-utils.d.ts.map +1 -1
  39. package/controls/element-utils.js +17 -0
  40. package/controls/index.d.ts +29 -8
  41. package/controls/index.d.ts.map +1 -1
  42. package/controls/index.js +26 -8
  43. package/controls/layer-list.d.ts +13 -5
  44. package/controls/layer-list.d.ts.map +1 -1
  45. package/controls/layer-list.js +72 -15
  46. package/controls/legend.d.ts +32 -5
  47. package/controls/legend.d.ts.map +1 -1
  48. package/controls/legend.js +95 -25
  49. package/controls/qualification.d.ts +152 -0
  50. package/controls/qualification.d.ts.map +1 -0
  51. package/controls/qualification.js +1265 -0
  52. package/controls/registry.d.ts +104 -0
  53. package/controls/registry.d.ts.map +1 -0
  54. package/controls/registry.js +150 -0
  55. package/controls/swipe-control.d.ts +6 -1
  56. package/controls/swipe-control.d.ts.map +1 -1
  57. package/controls/swipe-control.js +82 -20
  58. package/core/client.d.ts +45 -21
  59. package/core/client.d.ts.map +1 -1
  60. package/core/client.js +224 -46
  61. package/core/error-classifications.d.ts +6 -0
  62. package/core/error-classifications.d.ts.map +1 -1
  63. package/core/error-classifications.js +6 -0
  64. package/core/error-code-registry.d.ts +6 -0
  65. package/core/error-code-registry.d.ts.map +1 -1
  66. package/core/error-code-registry.js +6 -0
  67. package/core/error-envelope.js +2 -0
  68. package/core/errors.d.ts +1 -1
  69. package/core/errors.js +1 -1
  70. package/core/geoparquet-sql.d.ts +68 -4
  71. package/core/geoparquet-sql.d.ts.map +1 -1
  72. package/core/geoparquet-sql.js +325 -8
  73. package/core/odata-write-codec.d.ts.map +1 -1
  74. package/core/odata-write-codec.js +0 -1
  75. package/core/odata.d.ts +6 -0
  76. package/core/odata.d.ts.map +1 -1
  77. package/core/odata.js +6 -1
  78. package/core/ogc-processes.d.ts +182 -25
  79. package/core/ogc-processes.d.ts.map +1 -1
  80. package/core/ogc-processes.js +602 -30
  81. package/core/process-runner.d.ts +8 -1
  82. package/core/process-runner.d.ts.map +1 -1
  83. package/core/process-runner.js +1 -0
  84. package/core/protocol-transport.d.ts +2 -0
  85. package/core/protocol-transport.d.ts.map +1 -1
  86. package/core/stac.d.ts +31 -0
  87. package/core/stac.d.ts.map +1 -1
  88. package/core/stac.js +136 -19
  89. package/core/surfaces.d.ts.map +1 -1
  90. package/core/surfaces.js +273 -68
  91. package/core/types.d.ts +123 -9
  92. package/core/types.d.ts.map +1 -1
  93. package/core/wfs-axis-order.d.ts +18 -0
  94. package/core/wfs-axis-order.d.ts.map +1 -0
  95. package/core/wfs-axis-order.js +108 -0
  96. package/core/wfs-capabilities.d.ts +65 -24
  97. package/core/wfs-capabilities.d.ts.map +1 -1
  98. package/core/wfs-capabilities.js +237 -27
  99. package/core/wfs-filter.d.ts +26 -3
  100. package/core/wfs-filter.d.ts.map +1 -1
  101. package/core/wfs-filter.js +48 -53
  102. package/core/wfs-protocol-error.d.ts +12 -0
  103. package/core/wfs-protocol-error.d.ts.map +1 -0
  104. package/core/wfs-protocol-error.js +13 -0
  105. package/core/wfs.d.ts +53 -1
  106. package/core/wfs.d.ts.map +1 -1
  107. package/core/wfs.js +251 -29
  108. package/core/wms-feature-info.d.ts +92 -0
  109. package/core/wms-feature-info.d.ts.map +1 -0
  110. package/core/wms-feature-info.js +285 -0
  111. package/core/wms.d.ts +9 -0
  112. package/core/wms.d.ts.map +1 -1
  113. package/core/wms.js +17 -5
  114. package/esri-compat/event-bus.d.ts +20 -0
  115. package/esri-compat/event-bus.d.ts.map +1 -1
  116. package/esri-compat/feature-filter.d.ts +46 -1
  117. package/esri-compat/feature-filter.d.ts.map +1 -1
  118. package/esri-compat/feature-filter.js +49 -0
  119. package/esri-compat/geojson-layer.d.ts +4 -2
  120. package/esri-compat/geojson-layer.d.ts.map +1 -1
  121. package/esri-compat/geojson-layer.js +4 -2
  122. package/esri-compat/identity-manager.js +2 -2
  123. package/esri-compat/imagery-layer.d.ts +2 -1
  124. package/esri-compat/imagery-layer.d.ts.map +1 -1
  125. package/esri-compat/imagery-layer.js +2 -1
  126. package/esri-compat/layer-list.d.ts +8 -1
  127. package/esri-compat/layer-list.d.ts.map +1 -1
  128. package/esri-compat/layer-list.js +11 -4
  129. package/esri-compat/legend.d.ts.map +1 -1
  130. package/esri-compat/legend.js +1 -1
  131. package/esri-compat/locator.d.ts +297 -0
  132. package/esri-compat/locator.d.ts.map +1 -0
  133. package/esri-compat/locator.js +579 -0
  134. package/esri-compat/map-image-layer.d.ts.map +1 -1
  135. package/esri-compat/map-view.d.ts.map +1 -1
  136. package/esri-compat/map-view.js +1 -1
  137. package/esri-compat/scene-view.d.ts.map +1 -1
  138. package/esri-compat/time-slider.d.ts +29 -0
  139. package/esri-compat/time-slider.d.ts.map +1 -1
  140. package/esri-compat/time-slider.js +117 -0
  141. package/esri-compat/wfs-layer.d.ts +2 -1
  142. package/esri-compat/wfs-layer.d.ts.map +1 -1
  143. package/esri-compat/wfs-layer.js +2 -1
  144. package/esri-compat/widget-host.d.ts +42 -6
  145. package/esri-compat/widget-host.d.ts.map +1 -1
  146. package/esri-compat/widget-host.js +99 -10
  147. package/generated-app/projection.d.ts.map +1 -1
  148. package/generated-app/runtime.d.ts.map +1 -1
  149. package/map/data-to-map-bridge.d.ts +1 -1
  150. package/map/data-to-map-bridge.d.ts.map +1 -1
  151. package/map/data-to-map-bridge.js +1 -2
  152. package/map/honua-map.d.ts.map +1 -1
  153. package/map/honua-map.js +1 -1
  154. package/map/index.d.ts +2 -0
  155. package/map/index.d.ts.map +1 -1
  156. package/map/index.js +2 -0
  157. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  158. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  159. package/map/realtime-reconciliation-adapter.js +84 -0
  160. package/map/temporal-playback.d.ts +25 -0
  161. package/map/temporal-playback.d.ts.map +1 -1
  162. package/map/temporal-playback.js +29 -1
  163. package/operator/workspace/workspace.d.ts.map +1 -1
  164. package/operator/workspace/workspace.js +0 -2
  165. package/package.json +4 -3
  166. package/query-planner/canonical-filter.d.ts +30 -0
  167. package/query-planner/canonical-filter.d.ts.map +1 -0
  168. package/query-planner/canonical-filter.js +30 -0
  169. package/query-planner/columnar-plan.d.ts +63 -0
  170. package/query-planner/columnar-plan.d.ts.map +1 -0
  171. package/query-planner/columnar-plan.js +94 -0
  172. package/query-planner/diagnostics.d.ts +3 -3
  173. package/query-planner/diagnostics.d.ts.map +1 -1
  174. package/query-planner/diagnostics.js +17 -4
  175. package/query-planner/duckdb.d.ts.map +1 -1
  176. package/query-planner/duckdb.js +16 -2
  177. package/query-planner/execution-context.d.ts.map +1 -1
  178. package/query-planner/execution-context.js +3 -1
  179. package/query-planner/executor.d.ts.map +1 -1
  180. package/query-planner/executor.js +7 -1
  181. package/query-planner/geoservices.d.ts.map +1 -1
  182. package/query-planner/geoservices.js +24 -5
  183. package/query-planner/grpc.d.ts.map +1 -1
  184. package/query-planner/grpc.js +29 -5
  185. package/query-planner/index.d.ts +20 -5
  186. package/query-planner/index.d.ts.map +1 -1
  187. package/query-planner/index.js +16 -3
  188. package/query-planner/ir.d.ts +6 -1
  189. package/query-planner/ir.d.ts.map +1 -1
  190. package/query-planner/ir.js +63 -1
  191. package/query-planner/odata-protocol-module.d.ts +54 -0
  192. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  193. package/query-planner/odata-protocol-module.js +199 -0
  194. package/query-planner/odata-v1.d.ts +23 -0
  195. package/query-planner/odata-v1.d.ts.map +1 -0
  196. package/query-planner/odata-v1.js +202 -0
  197. package/query-planner/odata.d.ts +0 -3
  198. package/query-planner/odata.d.ts.map +1 -1
  199. package/query-planner/odata.js +0 -143
  200. package/query-planner/ogc-features.d.ts.map +1 -1
  201. package/query-planner/ogc-features.js +23 -1
  202. package/query-planner/planner.d.ts +2 -2
  203. package/query-planner/planner.d.ts.map +1 -1
  204. package/query-planner/planner.js +146 -27
  205. package/query-planner/representation.d.ts +59 -0
  206. package/query-planner/representation.d.ts.map +1 -0
  207. package/query-planner/representation.js +140 -0
  208. package/query-planner/types.d.ts +153 -3
  209. package/query-planner/types.d.ts.map +1 -1
  210. package/query-planner/wfs-protocol-module.d.ts +57 -0
  211. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  212. package/query-planner/wfs-protocol-module.js +731 -0
  213. package/query-planner/wfs-v1.d.ts +41 -0
  214. package/query-planner/wfs-v1.d.ts.map +1 -0
  215. package/query-planner/wfs-v1.js +366 -0
  216. package/query-planner/wfs.d.ts +1 -3
  217. package/query-planner/wfs.d.ts.map +1 -1
  218. package/query-planner/wfs.js +1 -70
  219. package/realtime/checkpoint-store.d.ts +206 -0
  220. package/realtime/checkpoint-store.d.ts.map +1 -0
  221. package/realtime/checkpoint-store.js +504 -0
  222. package/realtime/honua-server.d.ts +8 -0
  223. package/realtime/honua-server.d.ts.map +1 -1
  224. package/realtime/honua-server.js +123 -14
  225. package/realtime/index.d.ts +4 -0
  226. package/realtime/index.d.ts.map +1 -1
  227. package/realtime/index.js +2 -0
  228. package/realtime/reconciliation.d.ts +245 -0
  229. package/realtime/reconciliation.d.ts.map +1 -0
  230. package/realtime/reconciliation.js +377 -0
  231. package/realtime/resumable-transport.d.ts.map +1 -1
  232. package/realtime/sse.d.ts.map +1 -1
  233. package/realtime/websocket.d.ts.map +1 -1
  234. package/realtime/wire.d.ts +2 -1
  235. package/realtime/wire.d.ts.map +1 -1
  236. package/realtime/wire.js +24 -8
  237. package/replica-sync/errors.d.ts +19 -1
  238. package/replica-sync/errors.d.ts.map +1 -1
  239. package/replica-sync/errors.js +16 -0
  240. package/replica-sync/geoservices.d.ts +260 -0
  241. package/replica-sync/geoservices.d.ts.map +1 -0
  242. package/replica-sync/geoservices.js +1093 -0
  243. package/replica-sync/index.d.ts +5 -1
  244. package/replica-sync/index.d.ts.map +1 -1
  245. package/replica-sync/index.js +3 -1
  246. package/replica-sync/transport-conformance.d.ts +58 -0
  247. package/replica-sync/transport-conformance.d.ts.map +1 -0
  248. package/replica-sync/transport-conformance.js +265 -0
  249. package/replica-sync/types.d.ts +22 -0
  250. package/replica-sync/types.d.ts.map +1 -1
  251. package/runtime/index.d.ts +6 -2
  252. package/runtime/index.d.ts.map +1 -1
  253. package/runtime/index.js +3 -1
  254. package/runtime/load-package.d.ts +24 -0
  255. package/runtime/load-package.d.ts.map +1 -1
  256. package/runtime/load-package.js +45 -1
  257. package/runtime/maplibre-renderer.d.ts +11 -0
  258. package/runtime/maplibre-renderer.d.ts.map +1 -1
  259. package/runtime/maplibre-renderer.js +32 -7
  260. package/runtime/offline-region-protocol.d.ts +73 -0
  261. package/runtime/offline-region-protocol.d.ts.map +1 -0
  262. package/runtime/offline-region-protocol.js +63 -0
  263. package/runtime/offline-region-style.d.ts +78 -0
  264. package/runtime/offline-region-style.d.ts.map +1 -0
  265. package/runtime/offline-region-style.js +147 -0
  266. package/runtime/pmtiles-protocol.d.ts +7 -8
  267. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  268. package/runtime/pmtiles-protocol.js +19 -66
  269. package/runtime/protocol-registry.d.ts +55 -0
  270. package/runtime/protocol-registry.d.ts.map +1 -0
  271. package/runtime/protocol-registry.js +103 -0
  272. package/runtime/runtime.d.ts +2 -0
  273. package/runtime/runtime.d.ts.map +1 -1
  274. package/runtime/runtime.js +6 -1
  275. package/scene-workspace/analysis-widgets.d.ts +4 -2
  276. package/scene-workspace/analysis-widgets.d.ts.map +1 -1
  277. package/scene-workspace/analysis-widgets.js +4 -2
  278. package/scene-workspace/camera-correspondence.d.ts +151 -0
  279. package/scene-workspace/camera-correspondence.d.ts.map +1 -0
  280. package/scene-workspace/camera-correspondence.js +220 -0
  281. package/scene-workspace/cesium-adapter.d.ts +230 -29
  282. package/scene-workspace/cesium-adapter.d.ts.map +1 -1
  283. package/scene-workspace/cesium-adapter.js +741 -56
  284. package/scene-workspace/cesium-mount.d.ts +205 -0
  285. package/scene-workspace/cesium-mount.d.ts.map +1 -0
  286. package/scene-workspace/cesium-mount.js +392 -0
  287. package/scene-workspace/cesium-scene-owner.d.ts +160 -0
  288. package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
  289. package/scene-workspace/cesium-scene-owner.js +205 -0
  290. package/scene-workspace/cesium-state-sync.d.ts +101 -0
  291. package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
  292. package/scene-workspace/cesium-state-sync.js +432 -0
  293. package/scene-workspace/cesium-time.d.ts +346 -0
  294. package/scene-workspace/cesium-time.d.ts.map +1 -0
  295. package/scene-workspace/cesium-time.js +403 -0
  296. package/scene-workspace/index.d.ts +42 -10
  297. package/scene-workspace/index.d.ts.map +1 -1
  298. package/scene-workspace/index.js +32 -7
  299. package/scene-workspace/maplibre-state-sync.d.ts +112 -0
  300. package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
  301. package/scene-workspace/maplibre-state-sync.js +400 -0
  302. package/scene-workspace/primitives.d.ts +237 -3
  303. package/scene-workspace/primitives.d.ts.map +1 -1
  304. package/scene-workspace/primitives.js +1087 -17
  305. package/scene-workspace/scene-discovery.d.ts +5 -2
  306. package/scene-workspace/scene-discovery.d.ts.map +1 -1
  307. package/scene-workspace/scene-discovery.js +5 -2
  308. package/scene-workspace/scene-view.d.ts +5 -2
  309. package/scene-workspace/scene-view.d.ts.map +1 -1
  310. package/scene-workspace/scene-view.js +5 -2
  311. package/scene-workspace/source-to-cesium.d.ts +141 -8
  312. package/scene-workspace/source-to-cesium.d.ts.map +1 -1
  313. package/scene-workspace/source-to-cesium.js +497 -72
  314. package/scene-workspace/state-sync-port-kit.d.ts +156 -0
  315. package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
  316. package/scene-workspace/state-sync-port-kit.js +319 -0
  317. package/scene-workspace/state-sync.d.ts +34 -1
  318. package/scene-workspace/state-sync.d.ts.map +1 -1
  319. package/scene-workspace/state-sync.js +48 -0
  320. package/scene-workspace/types.d.ts +8 -0
  321. package/scene-workspace/types.d.ts.map +1 -1
  322. package/scene-workspace/types.js +2 -0
  323. package/scene-workspace/workspace.d.ts +12 -1
  324. package/scene-workspace/workspace.d.ts.map +1 -1
  325. package/scene-workspace/workspace.js +13 -0
  326. package/studio/index.d.ts +24 -7
  327. package/studio/index.d.ts.map +1 -1
  328. package/studio/index.js +21 -7
  329. package/studio/lifecycle-client.d.ts +164 -0
  330. package/studio/lifecycle-client.d.ts.map +1 -0
  331. package/studio/lifecycle-client.js +299 -0
  332. package/studio/lifecycle-errors.d.ts +91 -0
  333. package/studio/lifecycle-errors.d.ts.map +1 -0
  334. package/studio/lifecycle-errors.js +77 -0
  335. package/studio/lifecycle-types.d.ts +321 -0
  336. package/studio/lifecycle-types.d.ts.map +1 -0
  337. package/studio/lifecycle-types.js +22 -0
  338. package/web-components/controller.d.ts.map +1 -1
  339. package/web-components/csp-styles.d.ts +6 -0
  340. package/web-components/csp-styles.d.ts.map +1 -0
  341. package/web-components/csp-styles.js +53 -0
  342. package/web-components/elements.d.ts +158 -2
  343. package/web-components/elements.d.ts.map +1 -1
  344. package/web-components/elements.js +1302 -215
  345. package/web-components/export-redaction.d.ts +252 -0
  346. package/web-components/export-redaction.d.ts.map +1 -0
  347. package/web-components/export-redaction.js +588 -0
  348. package/web-components/export.d.ts +417 -0
  349. package/web-components/export.d.ts.map +1 -0
  350. package/web-components/export.js +719 -0
  351. package/web-components/feature-editor-model.d.ts +265 -0
  352. package/web-components/feature-editor-model.d.ts.map +1 -0
  353. package/web-components/feature-editor-model.js +438 -0
  354. package/web-components/feature-editor-sketch.d.ts +125 -0
  355. package/web-components/feature-editor-sketch.d.ts.map +1 -0
  356. package/web-components/feature-editor-sketch.js +194 -0
  357. package/web-components/feature-editor-workflow.d.ts +286 -0
  358. package/web-components/feature-editor-workflow.d.ts.map +1 -0
  359. package/web-components/feature-editor-workflow.js +889 -0
  360. package/web-components/feature-editor.d.ts +83 -0
  361. package/web-components/feature-editor.d.ts.map +1 -0
  362. package/web-components/feature-editor.js +829 -0
  363. package/web-components/feature-table-engine.d.ts +465 -0
  364. package/web-components/feature-table-engine.d.ts.map +1 -0
  365. package/web-components/feature-table-engine.js +1360 -0
  366. package/web-components/feature-table-view.d.ts +95 -0
  367. package/web-components/feature-table-view.d.ts.map +1 -0
  368. package/web-components/feature-table-view.js +343 -0
  369. package/web-components/index.d.ts +34 -3
  370. package/web-components/index.d.ts.map +1 -1
  371. package/web-components/index.js +35 -2
  372. package/web-components/maplibre-renderer.d.ts.map +1 -1
  373. package/web-components/maplibre-renderer.js +13 -4
  374. package/web-components/measurement.d.ts +3 -1
  375. package/web-components/measurement.d.ts.map +1 -1
  376. package/web-components/measurement.js +62 -18
  377. package/web-components/time-slider.d.ts +122 -0
  378. package/web-components/time-slider.d.ts.map +1 -0
  379. package/web-components/time-slider.js +740 -0
  380. package/web-components/types.d.ts +310 -0
  381. package/web-components/types.d.ts.map +1 -1
  382. package/webmap/convert-label.js +0 -1
  383. package/webmap/convert-symbol.d.ts.map +1 -1
@@ -0,0 +1,1265 @@
1
+ /**
2
+ * The **production qualification matrix** for the Honua custom-element kit
3
+ * (issue #683, parent epic #678; REQ-004 / REQ-005 / NFR-001).
4
+ *
5
+ * Epic #678's premise is that "production support" for a component library is
6
+ * not a claim anybody gets to make in prose. It is a matrix: every shipped
7
+ * component × every gate that matters, each cell carrying a status and, when
8
+ * the status is a claim of success, the automated evidence that backs it. This
9
+ * module is that matrix, and it extends the existing component catalog
10
+ * ({@link "./catalog.js"}) rather than introducing a second inventory —
11
+ * `HONUA_COMPONENT_CATALOG` remains the single list of shipped tags, and every
12
+ * catalog id must appear here exactly once (enforced by
13
+ * `scripts/component-qualification.mjs`).
14
+ *
15
+ * ## The honesty rules
16
+ *
17
+ * The matrix is seeded from what the test suite *actually* asserts today, not
18
+ * from what the components ought to do. Concretely:
19
+ *
20
+ * - `"passing"` requires at least one existing test file as evidence, and the
21
+ * CI verifier fails if any referenced file is missing. Deleting the test that
22
+ * backs a gate therefore breaks the build rather than quietly downgrading the
23
+ * claim.
24
+ * - `"failing"` is used where the requirement is *known* not to be met —
25
+ * including from code inspection, before an automated gate exists. It always
26
+ * carries a note naming the mechanism. This is deliberately more informative
27
+ * than `"pending"`: "we know the components hard-code English strings" is a
28
+ * different engineering state from "nobody has looked".
29
+ * - `"pending"` means no automated gate and no established verdict. It is the
30
+ * default, and most of this matrix is honestly in it.
31
+ * - `"not-applicable"` requires a note justifying why the gate cannot apply to
32
+ * that component — it is the only status that can silently remove work, so it
33
+ * is the one that most needs an argument attached.
34
+ *
35
+ * Nothing here is aspirational, and this matrix is a *different axis* from the
36
+ * catalog's `supportTier`. The tier records the functional maturity whichever
37
+ * feature issue (#680-#683) delivered on a component; this records the
38
+ * cross-cutting gates, which no component has cleared yet. The verifier enforces
39
+ * only the direction that is implied — clearing every gate requires the
40
+ * production tier, since gate completion is the harder bar — and surfaces every
41
+ * production-tier component's still-open gates rather than pretending the tier
42
+ * settles them. See {@link isComponentProductionQualified} and
43
+ * {@link listOpenQualificationGates}.
44
+ *
45
+ * ## Shape
46
+ *
47
+ * The declarations below are stored **inverted** — per gate, the groups of
48
+ * components that share a status and evidence — because that is how the
49
+ * evidence actually clusters (one test file typically proves one gate for
50
+ * several components) and because it keeps the reviewable diff small. The
51
+ * public API ({@link getComponentQualification},
52
+ * {@link listComponentQualifications}) expands it back into the dense
53
+ * per-component × per-gate matrix that consumers and the generated
54
+ * `config/component-qualification.v1.json` manifest want.
55
+ *
56
+ * Like `./catalog.js` this module is pure metadata: no DOM, no element classes,
57
+ * no registration, no `src/core` or `src/runtime` import. Importing it costs a
58
+ * consumer bundle nothing that NFR-001 forbids.
59
+ *
60
+ * @module
61
+ */
62
+ import { HONUA_COMPONENT_CATALOG } from "./catalog.js";
63
+ /**
64
+ * Bumped whenever a gate is added/removed or a status vocabulary changes, so
65
+ * the generated manifest and any external consumer can detect a shape change
66
+ * distinctly from a routine status update.
67
+ */
68
+ export const HONUA_COMPONENT_QUALIFICATION_DATA_VERSION = "1.1.0";
69
+ /** Every status, in escalating-confidence order. */
70
+ export const HONUA_COMPONENT_QUALIFICATION_STATUSES = Object.freeze([
71
+ "passing",
72
+ "failing",
73
+ "pending",
74
+ "not-applicable",
75
+ ]);
76
+ /** The full gate list, in a stable order. */
77
+ export const HONUA_COMPONENT_QUALIFICATION_GATES = Object.freeze([
78
+ {
79
+ id: "keyboard-behavior",
80
+ requirement: "REQ-004",
81
+ title: "Keyboard behavior",
82
+ criterion: "Every interactive affordance is operable from the keyboard alone, proven by dispatching real key events (not synthetic clicks) and asserting the resulting state change.",
83
+ },
84
+ {
85
+ id: "screen-reader-semantics",
86
+ requirement: "REQ-004",
87
+ title: "Screen-reader semantics",
88
+ criterion: "Roles, accessible names, state (pressed/checked/expanded/selected), and live regions are asserted on the rendered shadow tree.",
89
+ },
90
+ {
91
+ id: "focus-restoration",
92
+ requirement: "REQ-004",
93
+ title: "Focus restoration",
94
+ criterion: "A state-driven re-render preserves the active element and, for text fields, the caret/selection range — asserted, not assumed from the shared setShadowHtml() helper.",
95
+ },
96
+ {
97
+ id: "reduced-motion",
98
+ requirement: "REQ-004",
99
+ title: "Reduced motion",
100
+ criterion: "Any animation the component owns, whether CSS or a programmatic camera move, is suppressed or shortened under prefers-reduced-motion.",
101
+ },
102
+ {
103
+ id: "high-contrast",
104
+ requirement: "REQ-004",
105
+ title: "High contrast / forced colors",
106
+ criterion: "The component remains legible and its state remains distinguishable under forced-colors: active and prefers-contrast: more.",
107
+ },
108
+ {
109
+ id: "responsive-layout",
110
+ requirement: "REQ-004",
111
+ title: "Responsive layout",
112
+ criterion: "The component adapts to its container across the supported width range without clipping content or forcing horizontal overflow.",
113
+ },
114
+ {
115
+ id: "localization",
116
+ requirement: "REQ-004",
117
+ title: "Localization",
118
+ criterion: "Every user-visible string is externalized and resolvable through an injectable message source; no user-visible literal is hard-coded in the render template.",
119
+ },
120
+ {
121
+ id: "pseudo-locale",
122
+ requirement: "REQ-004",
123
+ title: "Pseudo-locale expansion",
124
+ criterion: "Rendering under an accented/expanded pseudo-locale neither truncates nor overflows any label, proving the layout tolerates real translation growth.",
125
+ },
126
+ {
127
+ id: "rtl",
128
+ requirement: "REQ-004",
129
+ title: "Right-to-left",
130
+ criterion: "Under dir=rtl the component mirrors correctly, using logical properties rather than physical left/right offsets.",
131
+ },
132
+ {
133
+ id: "visual-regression",
134
+ requirement: "REQ-005",
135
+ title: "Visual regression",
136
+ criterion: "A committed reference screenshot is compared per release, so unintended visual change fails CI.",
137
+ },
138
+ {
139
+ id: "strict-csp",
140
+ requirement: "REQ-005",
141
+ title: "Strict CSP",
142
+ criterion: "The component renders and functions under a strict Content-Security-Policy with no 'unsafe-inline' for script or style, producing no CSP violation reports.",
143
+ },
144
+ {
145
+ id: "zero-console-error",
146
+ requirement: "REQ-005",
147
+ title: "Zero console error",
148
+ criterion: "Mounting, driving, and unmounting the component emits no console.error or console.warn and raises no uncaught exception.",
149
+ },
150
+ {
151
+ id: "deterministic-disposal",
152
+ requirement: "REQ-005",
153
+ title: "Deterministic disposal",
154
+ criterion: "Disconnecting the element releases every subscription, listener, timer, and in-flight request it created, asserted by observing the collaborator's listener count drop to zero.",
155
+ },
156
+ {
157
+ id: "duplicate-listener",
158
+ requirement: "REQ-005",
159
+ title: "No duplicate listeners",
160
+ criterion: "Repeated re-renders do not accumulate handlers: a single user interaction after N renders still produces exactly one event.",
161
+ },
162
+ {
163
+ id: "memory-leak",
164
+ requirement: "REQ-005",
165
+ title: "Memory retention",
166
+ criterion: "A mounted-then-removed element is not retained, proven by a WeakRef/heap-retention probe after the controller is released.",
167
+ },
168
+ {
169
+ id: "ssr-import",
170
+ requirement: "REQ-005",
171
+ title: "SSR-safe import",
172
+ criterion: "The owning entrypoint imports cleanly with no DOM globals present, defining no custom element and touching no browser API at module scope.",
173
+ },
174
+ {
175
+ id: "secure-export",
176
+ requirement: "REQ-005",
177
+ title: "Secure export contract",
178
+ criterion: "Snapshot and state export actions fail closed without an explicit adapter, sanitize state and adapter output, preserve required provenance, and release adapter-owned resources exactly once; the built-in browser-print action is the documented safe exception.",
179
+ },
180
+ {
181
+ id: "bundle-budget",
182
+ requirement: "NFR-001",
183
+ title: "Per-entrypoint bundle budget",
184
+ criterion: "The owning kit's entrypoint carries a declared byte ceiling in bundle-budgets.json, enforced by npm run verify:bundle-budgets, and is forbidden from statically retaining a renderer, PDF writer, image encoder, localization framework, or test-only peer.",
185
+ },
186
+ ]);
187
+ // ── declarations ─────────────────────────────────────────────────────────
188
+ /** Catalog id groups referenced repeatedly below. */
189
+ const ALL_IDS = HONUA_COMPONENT_CATALOG.map((entry) => entry.id);
190
+ /** Every catalog id except the named ones, for gates where one component differs. */
191
+ function allExcept(...excluded) {
192
+ return ALL_IDS.filter((id) => !excluded.includes(id));
193
+ }
194
+ /**
195
+ * `<honua-feature-editor>` (issue #680) is the kit's first component the catalog
196
+ * declares `production-tier`, and it is the reason the tier cross-check below is
197
+ * one-directional. Its own suites are the strongest in the kit on the gates they
198
+ * cover: real keyboard events, a thorough ARIA contract, text-input focus
199
+ * restoration across a reconciling re-render, and — since issue #809 — the kit's
200
+ * only invocation-counting proofs of single-binding and full listener release.
201
+ *
202
+ * It is also the matrix earning its keep. Seeding it honestly recorded two
203
+ * `failing` cells naming a per-render shadow-root listener that accumulated and
204
+ * was never released on disconnect; #809 fixed exactly those two defects and
205
+ * added regression tests that count invocations rather than assert state, and
206
+ * both cells now flip to `passing` on that evidence. A matrix that had rounded
207
+ * those up to "probably fine" would have had nothing to flip.
208
+ */
209
+ const FEATURE_EDITOR = "web-components.feature-editor";
210
+ const FEATURE_EDITOR_SUITE = "test/web-components-feature-editor-element.test.ts";
211
+ /**
212
+ * `<honua-feature-table>`'s bounded production lane (issue #681) brought the
213
+ * kit's most complete ARIA-grid contract and its only two-lane keyboard suite,
214
+ * so several of its rows are re-seeded from that evidence rather than from the
215
+ * cross-cutting harness alone.
216
+ */
217
+ const FEATURE_TABLE = "web-components.feature-table";
218
+ const FEATURE_TABLE_SUITE = "test/web-components-feature-table-element.test.ts";
219
+ /**
220
+ * `<honua-time-slider>` (issue #959) is a view over the existing temporal
221
+ * playback controller rather than a controller-driven component, so it is not
222
+ * in the cross-cutting lifecycle harness — like `<honua-measurement>` it owns
223
+ * a collaborator-bound lifecycle proven by its own suite. It is also the kit's
224
+ * second ARIA slider (after `<honua-swipe-control>`), so its keyboard rows are
225
+ * seeded from real key events in both the jsdom and browser lanes.
226
+ */
227
+ const TIME_SLIDER = "web-components.time-slider";
228
+ const TIME_SLIDER_SUITE = "test/web-components-time-slider-element.test.ts";
229
+ /**
230
+ * The web-components tags covered by the cross-cutting lifecycle harness
231
+ * `test/web-components-lifecycle-gates.test.ts`. `web-components.map` is
232
+ * excluded because mounting it requires a real MapLibre renderer (its disposal
233
+ * is covered by the browser spec instead), and `web-components.measurement`
234
+ * because it owns a map-bound lifecycle proven by its own suite.
235
+ */
236
+ const LIFECYCLE_HARNESS_IDS = [
237
+ "web-components.layer-list",
238
+ "web-components.legend",
239
+ "web-components.feature-table",
240
+ "web-components.search",
241
+ "web-components.editor",
242
+ "web-components.chart",
243
+ "web-components.basemap-control",
244
+ "web-components.bookmarks",
245
+ "web-components.locate-control",
246
+ "web-components.measure-control",
247
+ "web-components.sketch-control",
248
+ "web-components.print-export",
249
+ "web-components.map-status",
250
+ "web-components.action-panel",
251
+ ];
252
+ const LIFECYCLE_HARNESS = "test/web-components-lifecycle-gates.test.ts";
253
+ const SSR_IMPORT_SUITE = "test/web-components-ssr-import.test.ts";
254
+ const BROWSER_SPEC = "test/playwright/web-components-basic.spec.mjs";
255
+ /**
256
+ * Read-only presentations: a legend and a chart render no interactive control in
257
+ * any state, so the interaction-driven gates have nothing to bind to. The
258
+ * harness asserts this in both directions, so adding an affordance to either
259
+ * forces these rows to move.
260
+ */
261
+ const DISPLAY_ONLY_IDS = ["web-components.legend", "web-components.chart"];
262
+ const CONTROLS_DISPLAY_ONLY_IDS = ["controls.legend"];
263
+ /** Harness tags that carry a real interactive affordance. */
264
+ const INTERACTIVE_HARNESS_IDS = LIFECYCLE_HARNESS_IDS.filter((id) => !DISPLAY_ONLY_IDS.includes(id));
265
+ /**
266
+ * Harness tags whose focused control survives a re-render. Determined
267
+ * empirically by the harness, which requires the complement to *lose* focus, so
268
+ * this set cannot drift away from observed behavior.
269
+ */
270
+ const FOCUS_RESTORING_IDS = [
271
+ "web-components.layer-list",
272
+ "web-components.feature-table",
273
+ "web-components.editor",
274
+ "web-components.basemap-control",
275
+ "web-components.bookmarks",
276
+ "web-components.measure-control",
277
+ "web-components.sketch-control",
278
+ "web-components.print-export",
279
+ "web-components.action-panel",
280
+ "web-components.locate-control",
281
+ "web-components.map-status",
282
+ // The search element's *text input* — the case that actually matters — is
283
+ // covered by the harness's dedicated text-field suite.
284
+ "web-components.search",
285
+ ];
286
+ /** No harness-covered interactive component currently loses focus across a re-render. */
287
+ const FOCUS_LOSING_IDS = [];
288
+ /** Components that move the map camera and would need to honour reduced motion. */
289
+ const CAMERA_MOVING_IDS = [
290
+ "web-components.map",
291
+ "web-components.search",
292
+ "web-components.bookmarks",
293
+ "web-components.locate-control",
294
+ ];
295
+ const DECLARATIONS = {
296
+ "keyboard-behavior": {
297
+ passing: [
298
+ {
299
+ ids: ["web-components.map"],
300
+ evidence: [BROWSER_SPEC],
301
+ note: "The focused zoom control receives a real Enter key event and produces the same viewport event count as its pointer-activation baseline in the browser fixture.",
302
+ },
303
+ {
304
+ ids: [
305
+ "web-components.basemap-control",
306
+ "web-components.bookmarks",
307
+ "web-components.locate-control",
308
+ "web-components.print-export",
309
+ "web-components.action-panel",
310
+ "web-components.map-status",
311
+ "web-components.measure-control",
312
+ "web-components.sketch-control",
313
+ "web-components.editor",
314
+ "controls.layer-list",
315
+ ],
316
+ evidence: [BROWSER_SPEC],
317
+ note: "The browser fixture dispatches real Space/Enter key events to each component's native checkbox, radio, or button and asserts the resulting layer, basemap, viewport, locate, export, or refresh state.",
318
+ },
319
+ {
320
+ ids: ["controls.basemap-switcher"],
321
+ evidence: ["test/controls/basemap-switcher.test.ts"],
322
+ note: "The native basemap switcher dispatches real ArrowLeft/ArrowRight/Home/End navigation events to its radiogroup and activates the focused choice with Enter and Space, asserting focus, checked state, and map selection.",
323
+ },
324
+ {
325
+ ids: ["web-components.search"],
326
+ evidence: ["test/web-components-search-element.test.ts"],
327
+ note: "ArrowDown/ArrowUp/Enter/Escape are dispatched as real KeyboardEvents against the combobox and the resulting suggestion, selection, and viewport changes are asserted.",
328
+ },
329
+ {
330
+ ids: ["web-components.measurement"],
331
+ evidence: ["test/web-components-measurement-element.test.ts"],
332
+ note: "Escape cancels an in-progress measurement via a real keydown.",
333
+ },
334
+ {
335
+ ids: [TIME_SLIDER],
336
+ evidence: [TIME_SLIDER_SUITE, BROWSER_SPEC],
337
+ note: "The full WAI-ARIA slider key set is dispatched as real KeyboardEvents at the scrubber and asserted against the controller it drives: ArrowRight/ArrowUp and ArrowLeft/ArrowDown step one window, PageUp/PageDown move ten, Home/End jump to the first/last window, the inline arrows reverse under dir=rtl, unhandled keys are asserted not to be preventDefault-ed, and the degraded (no controller) panel is asserted to consume nothing. Also driven end to end in Chromium, where the scrubber is reached by keyboard, stepped with arrow keys, and the resulting aria-valuenow and emitted honua-time-change window are asserted.",
338
+ },
339
+ {
340
+ ids: ["web-components.layer-list"],
341
+ evidence: [BROWSER_SPEC],
342
+ note: "The controller-driven layer list is exercised in Chromium with a focused visibility checkbox activated by Space and a focused reorder button activated by Enter; controller visibility/order state and the re-rendered shadow rows are asserted.",
343
+ },
344
+ {
345
+ ids: ["controls.swipe-control"],
346
+ evidence: ["test/controls/swipe-control.test.ts"],
347
+ note: "ArrowLeft/ArrowRight (with and without Shift), Home, and End step the divider and emit change.",
348
+ },
349
+ {
350
+ ids: [FEATURE_TABLE],
351
+ evidence: [FEATURE_TABLE_SUITE, BROWSER_SPEC],
352
+ note: "The kit's strongest keyboard coverage: ArrowDown/Enter/Tab dispatched as real KeyboardEvents against both the controller lane and the bounded engine lane, asserting the resulting focus move, selection, and — for Tab — that the grid does not preventDefault a key the page owns. Also verified end to end in a real browser. Not yet dispatched as DOM events: Home/End, Ctrl+Home/Ctrl+End, PageUp/PageDown, Space, and Shift+click additive sort; the key-to-move mapping behind the paging keys is unit-tested separately, so the gap is the DOM delivery path rather than the behavior.",
353
+ },
354
+ {
355
+ ids: [FEATURE_EDITOR],
356
+ evidence: [FEATURE_EDITOR_SUITE],
357
+ note: "Ctrl+Z / Ctrl+Shift+Z drive undo and redo and Escape cancels the draft, each dispatched as a real KeyboardEvent with the resulting workflow state asserted. The events are dispatched at the shadow root rather than from a focused control, so the root-level handler is proven but focus-based delivery is not.",
358
+ },
359
+ ],
360
+ notApplicable: [
361
+ {
362
+ ids: [...DISPLAY_ONLY_IDS, ...CONTROLS_DISPLAY_ONLY_IDS],
363
+ note: "These legend and chart components render display-only content and expose no interactive affordance for keyboard operation; adding one would require this row to be re-qualified.",
364
+ },
365
+ ],
366
+ pendingNote: "No key event is dispatched against this component in any suite. Where a test names keyboard operation it currently asserts a synthetic click on a natively focusable button, which proves the affordance exists but not that key handling works. jsdom does not implement a button's Enter/Space activation behavior, so this gate can only be closed in the browser lane for elements whose controls are plain buttons.",
367
+ },
368
+ "screen-reader-semantics": {
369
+ passing: [
370
+ {
371
+ ids: ["web-components.search"],
372
+ evidence: ["test/web-components-search-element.test.ts"],
373
+ note: "Full ARIA combobox contract: role, aria-autocomplete, aria-expanded, aria-controls, aria-activedescendant, listbox/option roles, aria-selected, and a role=status live region — plus the plain-textbox contract when no geocoder is assigned.",
374
+ },
375
+ {
376
+ ids: ["web-components.measurement"],
377
+ evidence: ["test/web-components-measurement-element.test.ts"],
378
+ note: "role=group mode set with aria-pressed state and a role=status aria-live=polite readout.",
379
+ },
380
+ {
381
+ ids: [TIME_SLIDER],
382
+ evidence: [TIME_SLIDER_SUITE],
383
+ note: "A complete WAI-ARIA slider contract: role=slider with aria-valuemin/aria-valuemax/aria-valuenow in epoch milliseconds tracking the controller's own extent and window, a human-readable aria-valuetext naming the window, aria-orientation, an accessible name derived from the panel label, and a pinned tabindex that is 0 while operable and -1 while degraded. Transport is a named role=group with aria-pressed on the play/pause toggle, the window readout is a role=status aria-live=polite region, and the degraded panel carries aria-disabled=true plus the host-supplied reason instead of an inert scrubber.",
384
+ },
385
+ {
386
+ ids: ["web-components.legend", "controls.legend"],
387
+ evidence: ["test/web-components-legend-element.test.ts", "test/controls/legend.test.ts"],
388
+ note: "List/listitem roles with aria-hidden swatches, asserted in both kits' implementations of honua-legend.",
389
+ },
390
+ {
391
+ ids: ["web-components.layer-list"],
392
+ evidence: ["test/web-components-layer-list-element.test.ts"],
393
+ note: "list/listitem roles per layer row plus accessible names on the opacity slider and reorder controls.",
394
+ },
395
+ {
396
+ ids: ["web-components.measure-control", "web-components.sketch-control"],
397
+ evidence: ["test/web-components-measure-sketch-elements.test.ts"],
398
+ note: "The provider-absent affordance is asserted to carry aria-disabled=true rather than a silently inert button.",
399
+ },
400
+ {
401
+ ids: ["web-components.print-export"],
402
+ evidence: [LIFECYCLE_HARNESS],
403
+ note: "The outcome readout is a role=status aria-live=polite region that names the adapter requirement, and an export with no adapter carries an explanatory title. Deliberately neither disabled nor aria-disabled: both suppress activation, and activation is how an application discovers it must supply an adapter — the asserted behavior is that activating an unavailable export still reports the unsupported outcome through honua-export.",
404
+ },
405
+ {
406
+ ids: [FEATURE_EDITOR],
407
+ evidence: [FEATURE_EDITOR_SUITE],
408
+ note: "The kit's most complete accessibility contract: per-field label association, aria-required, aria-invalid with aria-describedby resolving to the validation text, role=alert on validation/conflict/failure regions, aria-pressed on operation and sketch-tool toggles, aria-disabled on unavailable actions, role=group with an accessible name on the tool groups, and a role=status aria-live=polite readout.",
409
+ },
410
+ {
411
+ ids: [FEATURE_TABLE],
412
+ evidence: [FEATURE_TABLE_SUITE],
413
+ note: "A full WAI-ARIA grid contract: role=grid/row/columnheader/gridcell with absolute aria-rowcount, aria-colcount, aria-rowindex, and aria-colindex across a virtualized window, aria-rowcount=-1 when the true total is genuinely unknown rather than a fabricated number, aria-sort, aria-selected, aria-busy, a role=status aria-live=polite readout, and placeholder rows that announce nothing they cannot substantiate. The grid's own accessible name (aria-labelledby) is emitted but not asserted.",
414
+ },
415
+ {
416
+ ids: ["controls.swipe-control", "controls.layer-list"],
417
+ evidence: ["test/controls/accessibility.test.ts"],
418
+ note: "The native controls expose explicit accessible structure: a labelled slider with bounded value state and a labelled group containing native checkbox controls paired with their visible labels.",
419
+ },
420
+ {
421
+ ids: ["controls.basemap-switcher"],
422
+ evidence: ["test/controls/basemap-switcher.test.ts"],
423
+ note: "The native basemap switcher exposes a labelled radiogroup whose choices have radio roles, stable accessible names, and checked state that tracks the selected basemap.",
424
+ },
425
+ {
426
+ ids: [
427
+ "web-components.map",
428
+ "web-components.editor",
429
+ "web-components.chart",
430
+ "web-components.basemap-control",
431
+ "web-components.bookmarks",
432
+ "web-components.locate-control",
433
+ "web-components.map-status",
434
+ "web-components.action-panel",
435
+ ],
436
+ evidence: ["test/web-components-accessibility.test.ts"],
437
+ note: "The web components expose named panels or regions, native action controls with accessible names, and live status content where component state changes are announced.",
438
+ },
439
+ ],
440
+ pendingNote: "No role, accessible-name, or ARIA-state assertion exists for this component. Several are exercised by role-based Playwright locators on the browser fixture page, which depends on accessible names but does not assert the full semantic contract.",
441
+ },
442
+ "focus-restoration": {
443
+ passing: [
444
+ {
445
+ ids: FOCUS_RESTORING_IDS,
446
+ evidence: [LIFECYCLE_HARNESS],
447
+ note: "The shared setShadowHtml() capture/restore path is asserted per tag: the focused control survives a controller-driven re-render. For the search element the covered case is its text input, which additionally keeps its typed value and selection range.",
448
+ },
449
+ {
450
+ ids: ["web-components.search"],
451
+ evidence: ["test/web-components-search-element.test.ts", BROWSER_SPEC],
452
+ note: "Also verified end to end in a real browser: the search input keeps focus across the re-render that follows a suggestion commit.",
453
+ },
454
+ {
455
+ ids: [FEATURE_TABLE],
456
+ evidence: [FEATURE_TABLE_SUITE],
457
+ note: "Stronger than the harness's generic check: the exact cell keeps focus across the re-render that focusing itself causes, via a data-cell-key attribute deliberately ordered ahead of data-field so the base class's alphabetical data-* scan selects the cell rather than the column. The roving tabindex is restored with it, and rows are asserted to stay out of the tab sequence so the grid keeps a single tab stop.",
458
+ },
459
+ {
460
+ ids: [FEATURE_EDITOR],
461
+ evidence: [FEATURE_EDITOR_SUITE],
462
+ note: "A reconciling realtime change forces a re-render and the focused field keeps its edited value, its focus, and its caret range. Covered for text inputs only: this element hand-rolls its own captureFocus() keyed on `id` alone rather than using the shared setShadowHtml() helper's id/name/data-* selector, and none of its buttons carry an id, so button focus is silently lost across the same re-render.",
463
+ },
464
+ {
465
+ ids: ["controls.basemap-switcher", "controls.swipe-control"],
466
+ evidence: ["test/controls/accessibility.test.ts"],
467
+ note: "A focused basemap radio and swipe divider retain focus across their state rerenders.",
468
+ },
469
+ {
470
+ ids: ["web-components.measurement"],
471
+ evidence: ["test/web-components-measurement-element.test.ts"],
472
+ note: "The focused measurement mode button retains focus when the mode state rerenders.",
473
+ },
474
+ {
475
+ ids: [TIME_SLIDER],
476
+ evidence: [TIME_SLIDER_SUITE],
477
+ note: "Structurally different from the rest of the kit and asserted as such: this element builds its shadow tree once and updates every value in place, so a playback tick replaces no node. The focused scrubber is asserted to still be shadowRoot.activeElement after ticks, a speed change, and a messages/label change, and the node identity is asserted unchanged across the same updates — which is the property that also keeps an in-flight pointer drag from being torn out. A structural rebuild (the speed control appearing or its options changing) does replace the tree and is not covered.",
478
+ },
479
+ {
480
+ ids: ["web-components.map"],
481
+ evidence: [BROWSER_SPEC],
482
+ note: "A focused zoom control retains focus across repeated controller-driven map state updates in the real MapLibre browser fixture.",
483
+ },
484
+ {
485
+ ids: ["controls.layer-list"],
486
+ evidence: ["test/controls/accessibility.test.ts"],
487
+ note: "A focused layer checkbox retains focus across an overlays rerender.",
488
+ },
489
+ ],
490
+ failing: [
491
+ {
492
+ ids: FOCUS_LOSING_IDS,
493
+ note: "Focus is lost across a re-render, asserted by the harness rather than assumed: this element's control carries no id, name, or non-empty data-* attribute, so the base class's focusSelector() cannot find it again after innerHTML is replaced. The search element's submit button has the same defect, though its text input (the case that matters) is covered.",
494
+ },
495
+ ],
496
+ notApplicable: [
497
+ {
498
+ ids: DISPLAY_ONLY_IDS,
499
+ note: "A read-only presentation with no focusable control; there is no active element to preserve. The harness asserts the absence, so adding an affordance moves this row.",
500
+ },
501
+ {
502
+ ids: CONTROLS_DISPLAY_ONLY_IDS,
503
+ note: "The native legend is a read-only list presentation with no focusable control; there is no active element to preserve.",
504
+ },
505
+ ],
506
+ pendingNote: "The controls kit renders through its own template path rather than the web-components base class's capture/restore helper, and no test asserts focus survives a re-render there.",
507
+ },
508
+ "reduced-motion": {
509
+ passing: [
510
+ {
511
+ ids: CAMERA_MOVING_IDS,
512
+ evidence: ["test/runtime/runtime.test.ts"],
513
+ note: "The shared runtime forces animated bounding-box camera transitions to animate:false when the map container reports prefers-reduced-motion: reduce, covering all four web components that delegate camera commits through this path.",
514
+ },
515
+ ],
516
+ notApplicable: [
517
+ {
518
+ ids: ALL_IDS.filter((id) => !CAMERA_MOVING_IDS.includes(id)),
519
+ note: "The component owns no animation: its shadow styles declare no transition, animation, or keyframes, and it does not move the map camera. There is nothing for prefers-reduced-motion to suppress. Re-evaluate if animated affordances are added.",
520
+ },
521
+ ],
522
+ },
523
+ "high-contrast": {
524
+ passing: [
525
+ {
526
+ ids: [FEATURE_EDITOR],
527
+ evidence: [FEATURE_EDITOR_SUITE],
528
+ note: "The feature editor declares forced-colors and prefers-contrast styles for pressed, disabled, native form, invalid, and validation states, with the emitted stylesheet assertions covering system Canvas/Highlight colors and non-color state distinctions.",
529
+ },
530
+ {
531
+ ids: ["controls.swipe-control"],
532
+ evidence: ["test/controls/accessibility.test.ts"],
533
+ note: "The swipe divider declares forced-colors and prefers-contrast rules that preserve the divider and handle boundary using ButtonText/Canvas system colors, asserted from the emitted shadow stylesheet.",
534
+ },
535
+ {
536
+ ids: ["controls.layer-list"],
537
+ evidence: ["test/controls/layer-list.test.ts"],
538
+ note: "The native layer list declares forced-colors and prefers-contrast rules that preserve checked-row distinction with a CanvasText outline and unseeded-row distinction with GrayText, asserted from the component stylesheet.",
539
+ },
540
+ {
541
+ ids: ["controls.basemap-switcher"],
542
+ evidence: ["test/controls/accessibility.test.ts"],
543
+ note: "The native basemap switcher declares a forced-colors/prefers-contrast Highlight outline for the active radio, asserted from the emitted stylesheet.",
544
+ },
545
+ {
546
+ ids: ["controls.legend"],
547
+ evidence: ["test/controls/legend.test.ts"],
548
+ note: "The native legend declares forced-colors/prefers-contrast system-color rules for swatches and the empty state, asserted from the component stylesheet.",
549
+ },
550
+ {
551
+ ids: ["web-components.search"],
552
+ evidence: ["test/web-components-search-element.test.ts"],
553
+ note: "The search component declares forced-colors/prefers-contrast system-color rules for borders and selected suggestions, asserted from the emitted stylesheet.",
554
+ },
555
+ {
556
+ ids: ["web-components.legend"],
557
+ evidence: ["test/web-components-legend-element.test.ts"],
558
+ note: "The web-component legend declares forced-colors/prefers-contrast system-color rules that preserve swatch boundaries and label contrast, asserted from the emitted stylesheet.",
559
+ },
560
+ {
561
+ ids: ["web-components.map-status"],
562
+ evidence: ["test/web-components-map-status-element.test.ts"],
563
+ note: "The map-status panel declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonText/GrayText/Mark system-color rules for normal, unsupported, error, and action states, asserted from the emitted stylesheet.",
564
+ },
565
+ {
566
+ ids: ["web-components.action-panel"],
567
+ evidence: ["test/web-components-action-panel-element.test.ts"],
568
+ note: "The action panel declares forced-colors/prefers-contrast system-color rules for panel, action, empty, and status states, asserted from the emitted stylesheet.",
569
+ },
570
+ {
571
+ ids: ["web-components.layer-list"],
572
+ evidence: ["test/web-components-layer-list-element.test.ts"],
573
+ note: "The web-component layer list declares forced-colors/prefers-contrast system-color rules for row boundaries, checked labels, and reorder controls, asserted from the emitted stylesheet.",
574
+ },
575
+ {
576
+ ids: ["web-components.bookmarks"],
577
+ evidence: ["test/web-components-accessibility.test.ts"],
578
+ note: "The bookmarks control panel declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonFace/ButtonText rules for its surface and action buttons, asserted from the emitted stylesheet.",
579
+ },
580
+ {
581
+ ids: ["web-components.basemap-control"],
582
+ evidence: ["test/web-components-basemap-control-element.test.ts"],
583
+ note: "The web basemap control declares forced-colors/prefers-contrast system-color rules for active radio state and controls, asserted from the emitted stylesheet.",
584
+ },
585
+ {
586
+ ids: ["web-components.locate-control"],
587
+ evidence: ["test/web-components-accessibility.test.ts"],
588
+ note: "The locate control's shared panel declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonFace/ButtonText rules for its surface and action, asserted from the emitted stylesheet.",
589
+ },
590
+ {
591
+ ids: ["web-components.measure-control", "web-components.sketch-control"],
592
+ evidence: ["test/web-components-accessibility.test.ts"],
593
+ note: "The measure and sketch controls' shared panels declare forced-colors/prefers-contrast Canvas/CanvasText/ButtonFace/ButtonText rules, asserted from each emitted stylesheet.",
594
+ },
595
+ {
596
+ ids: ["web-components.print-export"],
597
+ evidence: ["test/web-components-accessibility.test.ts"],
598
+ note: "The print/export control declares forced-colors/prefers-contrast system-color rules for its panel and export actions, asserted from the emitted stylesheet.",
599
+ },
600
+ {
601
+ ids: ["web-components.editor"],
602
+ evidence: ["test/web-components-accessibility.test.ts"],
603
+ note: "The web editor declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonText rules for its panel and native actions, asserted from the emitted stylesheet.",
604
+ },
605
+ {
606
+ ids: ["web-components.measurement"],
607
+ evidence: ["test/web-components-measurement-element.test.ts"],
608
+ note: "The measurement element declares forced-colors/prefers-contrast system-color rules for its mode/status surface, asserted from the emitted stylesheet.",
609
+ },
610
+ {
611
+ ids: [TIME_SLIDER],
612
+ evidence: [TIME_SLIDER_SUITE],
613
+ note: "The time slider declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonFace/ButtonText/Highlight/GrayText rules for its panel, transport buttons, pressed play state, and — the case colour alone would lose — the scrubber track, elapsed fill, thumb, focus ring, and disabled state, with forced-color-adjust disabled on the track so the value stays visible; asserted from the emitted stylesheet.",
614
+ },
615
+ {
616
+ ids: ["web-components.chart"],
617
+ evidence: ["test/web-components-accessibility.test.ts"],
618
+ note: "The chart declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonFace/ButtonText/Highlight rules for its panel and bars, asserted from the emitted stylesheet.",
619
+ },
620
+ {
621
+ ids: ["web-components.feature-table"],
622
+ evidence: ["test/web-components-accessibility.test.ts"],
623
+ note: "The feature table declares forced-colors/prefers-contrast Canvas/CanvasText/Highlight rules for panel, headers, selected rows, and focus, asserted from the emitted stylesheet.",
624
+ },
625
+ {
626
+ ids: ["web-components.map"],
627
+ evidence: ["test/web-components-accessibility.test.ts"],
628
+ note: "The map declares forced-colors/prefers-contrast Canvas/CanvasText/ButtonText rules for its panel, chrome, canvas, status, and controls, asserted from the emitted stylesheet.",
629
+ },
630
+ ],
631
+ failing: [
632
+ {
633
+ ids: ALL_IDS.filter((id) => id !== FEATURE_EDITOR &&
634
+ id !== "controls.swipe-control" &&
635
+ id !== "controls.layer-list" &&
636
+ id !== "controls.basemap-switcher" &&
637
+ id !== "controls.legend" &&
638
+ id !== "web-components.search" &&
639
+ id !== "web-components.legend" &&
640
+ id !== "web-components.map-status" &&
641
+ id !== "web-components.action-panel" &&
642
+ id !== "web-components.layer-list" &&
643
+ id !== "web-components.bookmarks" &&
644
+ id !== "web-components.basemap-control" &&
645
+ id !== "web-components.locate-control" &&
646
+ id !== "web-components.measure-control" &&
647
+ id !== "web-components.sketch-control" &&
648
+ id !== "web-components.print-export" &&
649
+ id !== "web-components.editor" &&
650
+ id !== "web-components.measurement" &&
651
+ id !== "web-components.chart" &&
652
+ id !== "web-components.feature-table" &&
653
+ id !== TIME_SLIDER &&
654
+ id !== "web-components.map"),
655
+ note: "Shadow styles hard-code foreground/background/border colors with no forced-colors: active or prefers-contrast: more block, so state conveyed by color (selected rows, pressed modes, legend swatches, disabled buttons) collapses under a forced-colors palette.",
656
+ },
657
+ ],
658
+ },
659
+ "responsive-layout": {
660
+ passing: [
661
+ {
662
+ ids: ["web-components.search"],
663
+ evidence: ["test/web-components-search-element.test.ts"],
664
+ note: "The search form uses a shrinkable grid and stacks its submit button full-width below 320px, asserted from the emitted stylesheet for narrow-container behavior.",
665
+ },
666
+ {
667
+ ids: ["controls.basemap-switcher"],
668
+ evidence: ["test/controls/accessibility.test.ts"],
669
+ note: "The native basemap switcher stacks its wrapped group and ellipsizes radio labels below 240px, asserted from the emitted stylesheet.",
670
+ },
671
+ {
672
+ ids: ["web-components.map-status"],
673
+ evidence: ["test/web-components-accessibility.test.ts"],
674
+ note: "The map-status row stacks its content and makes the action full-width below 240px, asserted from the emitted stylesheet.",
675
+ },
676
+ {
677
+ ids: ["web-components.feature-table"],
678
+ evidence: ["test/web-components-feature-table-element.test.ts"],
679
+ note: "The feature table keeps its panel shrinkable and exposes horizontal scrolling for narrow containers, with an overflow-safe table width, asserted from the emitted stylesheet.",
680
+ },
681
+ {
682
+ ids: [
683
+ "web-components.basemap-control",
684
+ "web-components.bookmarks",
685
+ "web-components.locate-control",
686
+ "web-components.measure-control",
687
+ "web-components.sketch-control",
688
+ "web-components.print-export",
689
+ "web-components.action-panel",
690
+ ],
691
+ evidence: ["test/web-components-accessibility.test.ts"],
692
+ note: "The shared control-panel stylesheet removes its fixed minimum width, stacks the heading, wraps long labels, and makes narrow segmented/action controls full-width below 240px; each covered panel asserts the emitted stylesheet.",
693
+ },
694
+ {
695
+ ids: ["web-components.editor"],
696
+ evidence: ["test/web-components-accessibility.test.ts"],
697
+ note: "The editor panel makes its action row wrap and its buttons shrink below 320px, asserted from the emitted stylesheet.",
698
+ },
699
+ {
700
+ ids: ["controls.swipe-control", "controls.legend", "controls.layer-list"],
701
+ evidence: [
702
+ "test/controls/swipe-control.test.ts",
703
+ "test/controls/legend.test.ts",
704
+ "test/controls/layer-list.test.ts",
705
+ ],
706
+ note: "The native controls constrain their panel and row widths, allow flex children to shrink, and wrap unbroken labels without narrow-container overflow, asserted from each emitted stylesheet.",
707
+ },
708
+ {
709
+ ids: [
710
+ "web-components.map",
711
+ "web-components.layer-list",
712
+ "web-components.legend",
713
+ "web-components.feature-editor",
714
+ "web-components.chart",
715
+ "web-components.measurement",
716
+ ],
717
+ evidence: [
718
+ "test/web-components-map-status-element.test.ts",
719
+ "test/web-components-layer-list-element.test.ts",
720
+ "test/web-components-legend-element.test.ts",
721
+ "test/web-components-feature-editor-element.test.ts",
722
+ "test/web-components-lifecycle-gates.test.ts",
723
+ "test/web-components-measurement-element.test.ts",
724
+ ],
725
+ note: "The remaining web components emit narrow-container rules that remove fixed minimum widths, stack or wrap controls, and keep bars, layer tools, legends, and measurement content within the available inline size, asserted from focused emitted-style tests.",
726
+ },
727
+ {
728
+ ids: [TIME_SLIDER],
729
+ evidence: [TIME_SLIDER_SUITE],
730
+ note: "Below 320px the time slider drops its fixed minimum width, stacks the heading bar, collapses the transport row to one full-width column, and stacks the speed label above a full-width select; the scrubber track is capped at 100% inline size and the window readout and hint wrap rather than overflow, asserted from the emitted stylesheet.",
731
+ },
732
+ ],
733
+ failing: [
734
+ {
735
+ ids: ALL_IDS.filter((id) => id !== "web-components.search" &&
736
+ id !== "controls.basemap-switcher" &&
737
+ id !== "web-components.map-status" &&
738
+ id !== "web-components.feature-table" &&
739
+ id !== "web-components.basemap-control" &&
740
+ id !== "web-components.bookmarks" &&
741
+ id !== "web-components.locate-control" &&
742
+ id !== "web-components.measure-control" &&
743
+ id !== "web-components.sketch-control" &&
744
+ id !== "web-components.print-export" &&
745
+ id !== "web-components.action-panel" &&
746
+ id !== "web-components.editor" &&
747
+ id !== "controls.swipe-control" &&
748
+ id !== "controls.legend" &&
749
+ id !== "controls.layer-list" &&
750
+ id !== "web-components.map" &&
751
+ id !== "web-components.layer-list" &&
752
+ id !== "web-components.legend" &&
753
+ id !== "web-components.feature-editor" &&
754
+ id !== "web-components.chart" &&
755
+ id !== TIME_SLIDER &&
756
+ id !== "web-components.measurement"),
757
+ note: "The remaining components do not yet declare an @media or @container rule; their narrow layouts remain unqualified until focused emitted-style evidence exists.",
758
+ },
759
+ ],
760
+ },
761
+ localization: {
762
+ passing: [
763
+ {
764
+ ids: ["controls.basemap-switcher"],
765
+ evidence: ["test/controls/accessibility.test.ts"],
766
+ note: "The basemap switcher accepts a caller-supplied non-English group label and emits it as the radiogroup accessible name; individual basemap names are supplied by the base definitions.",
767
+ },
768
+ {
769
+ ids: ["controls.swipe-control"],
770
+ evidence: ["test/controls/swipe-control.test.ts"],
771
+ note: "The swipe control has no default accessible label and reflects caller-supplied label text through its public property/attribute into aria-label, asserted with French and German labels.",
772
+ },
773
+ {
774
+ ids: ["controls.legend"],
775
+ evidence: ["test/controls/legend.test.ts"],
776
+ note: "The native legend accepts a caller-supplied accessible label and empty-state fallback text, asserted with German rendering coverage while preserving caller-supplied legend entries.",
777
+ },
778
+ {
779
+ ids: ["controls.layer-list"],
780
+ evidence: ["test/controls/layer-list.test.ts"],
781
+ note: "The native layer list accepts caller-supplied accessible, unseeded, and fallback labels, asserted with German rendering coverage while preserving overlay labels supplied by the caller.",
782
+ },
783
+ {
784
+ ids: ["web-components.map"],
785
+ evidence: ["test/web-components-map-status-element.test.ts"],
786
+ note: "The map accepts caller-supplied control, zoom, layer-count, center, package-loading, and no-package messages while preserving the map label, asserted with German rendering coverage.",
787
+ },
788
+ {
789
+ ids: ["web-components.locate-control"],
790
+ evidence: ["test/web-components-locate-control.test.ts"],
791
+ note: "The locate control accepts a caller-supplied label and typed message source for action text, statuses, initial/unavailable copy, and geolocation state messages, asserted with German rendering coverage.",
792
+ },
793
+ {
794
+ ids: ["web-components.editor"],
795
+ evidence: ["test/web-components-accessibility.test.ts"],
796
+ note: "The editor accepts caller-supplied action attributes and typed status, selection, editable, and read-only messages, including a reason formatter, asserted with German rendering coverage.",
797
+ },
798
+ {
799
+ ids: ["web-components.bookmarks"],
800
+ evidence: ["test/web-components-accessibility.test.ts"],
801
+ note: "The bookmarks component accepts caller-supplied status and empty-state messages, asserted with German rendering coverage while preserving bookmark labels supplied by the caller.",
802
+ },
803
+ {
804
+ ids: ["web-components.action-panel"],
805
+ evidence: ["test/web-components-action-panel-element.test.ts"],
806
+ note: "The action panel accepts caller-supplied label, status, and empty-state messages while preserving action labels supplied by the caller, asserted with German rendering coverage.",
807
+ },
808
+ {
809
+ ids: ["web-components.measure-control"],
810
+ evidence: ["test/web-components-measure-sketch-elements.test.ts"],
811
+ note: "The measure control accepts caller-supplied status, action, and empty-state messages while preserving mode labels supplied by the caller, asserted with German rendering coverage.",
812
+ },
813
+ {
814
+ ids: ["web-components.print-export"],
815
+ evidence: ["test/web-components-lifecycle-gates.test.ts"],
816
+ note: "The print/export control accepts caller-supplied status, action, and unsupported-state messages while preserving export labels supplied by the caller, asserted with German rendering coverage.",
817
+ },
818
+ {
819
+ ids: ["web-components.sketch-control"],
820
+ evidence: ["test/web-components-measure-sketch-elements.test.ts"],
821
+ note: "The sketch control accepts caller-supplied status, action, and empty-state messages while preserving tool labels supplied by the caller, asserted with German rendering coverage.",
822
+ },
823
+ {
824
+ ids: [
825
+ "web-components.layer-list",
826
+ "web-components.legend",
827
+ "web-components.feature-table",
828
+ "web-components.search",
829
+ "web-components.feature-editor",
830
+ "web-components.chart",
831
+ "web-components.basemap-control",
832
+ "web-components.measurement",
833
+ "web-components.map-status",
834
+ TIME_SLIDER,
835
+ ],
836
+ evidence: ["test/web-components-localization-german.test.ts"],
837
+ note: "The remaining web components expose typed caller-injected messages for user-visible labels, statuses, empty states, placeholders, and ARIA text, preserving legacy defaults and asserting German rendering coverage.",
838
+ },
839
+ ],
840
+ failing: [
841
+ {
842
+ ids: allExcept("web-components.search", "controls.basemap-switcher", "controls.swipe-control", "controls.legend", "controls.layer-list", "web-components.map", "web-components.locate-control", "web-components.editor", "web-components.bookmarks", "web-components.action-panel", "web-components.measure-control", "web-components.print-export", "web-components.sketch-control", "web-components.layer-list", "web-components.legend", "web-components.feature-table", "web-components.search", "web-components.feature-editor", "web-components.chart", "web-components.basemap-control", "web-components.measurement", "web-components.map-status", TIME_SLIDER),
843
+ note: "Every user-visible string is a hard-coded English literal inside the render template (button labels, status words, empty-state copy, accessible names). There is no message source to inject and no locale plumbing in either kit.",
844
+ },
845
+ ],
846
+ },
847
+ "pseudo-locale": {
848
+ passing: [
849
+ {
850
+ ids: ALL_IDS,
851
+ evidence: ["test/playwright/pseudo-locale.spec.mjs"],
852
+ note: "The browser probe expands every visible alphabetic label by approximately 35% at a 280px viewport and finds no clipping or truncation across the complete component catalog.",
853
+ },
854
+ ],
855
+ },
856
+ rtl: {
857
+ passing: [
858
+ {
859
+ ids: [FEATURE_EDITOR],
860
+ evidence: [FEATURE_EDITOR_SUITE],
861
+ note: "The feature editor renders flex/grid surfaces without physical left/right declarations; an explicit dir=rtl fixture asserts the emitted stylesheet remains direction-neutral.",
862
+ },
863
+ {
864
+ ids: ["controls.swipe-control"],
865
+ evidence: ["test/controls/swipe-control.test.ts"],
866
+ note: "The swipe control uses logical divider positioning, reverses inline pointer and keyboard movement in RTL, and asserts the RTL clip path and Home/End semantics in focused tests.",
867
+ },
868
+ {
869
+ ids: ["controls.layer-list"],
870
+ evidence: ["test/controls/layer-list.test.ts"],
871
+ note: "The native layer list renders direction-neutral flex rows under an explicit dir=rtl fixture and has no physical left/right style declarations.",
872
+ },
873
+ {
874
+ ids: ["controls.basemap-switcher"],
875
+ evidence: ["test/controls/accessibility.test.ts"],
876
+ note: "The native basemap switcher is rendered with dir=rtl and its emitted structural styles contain no physical left/right declarations.",
877
+ },
878
+ {
879
+ ids: ["controls.legend"],
880
+ evidence: ["test/controls/legend.test.ts"],
881
+ note: "The native legend inherits an explicit RTL direction and uses logical label alignment, asserted through its rendered direction and stylesheet coverage.",
882
+ },
883
+ {
884
+ ids: ["web-components.map-status"],
885
+ evidence: ["test/web-components-map-status-element.test.ts"],
886
+ note: "The map-status component inherits RTL direction and uses logical padding and start-aligned text, asserted with Arabic rendering and stylesheet coverage.",
887
+ },
888
+ {
889
+ ids: [
890
+ "web-components.map",
891
+ "web-components.layer-list",
892
+ "web-components.legend",
893
+ "web-components.feature-table",
894
+ "web-components.chart",
895
+ ],
896
+ evidence: ["test/web-components-rtl.test.ts"],
897
+ note: "The map, layer list, legend, feature table, and chart render under dir=rtl with logical direction, padding, inset, alignment, and chart-fill styles, asserted with Arabic/RTL rendering coverage.",
898
+ },
899
+ {
900
+ ids: [
901
+ "web-components.search",
902
+ "web-components.editor",
903
+ "web-components.basemap-control",
904
+ "web-components.bookmarks",
905
+ "web-components.locate-control",
906
+ "web-components.measure-control",
907
+ "web-components.measurement",
908
+ "web-components.sketch-control",
909
+ "web-components.print-export",
910
+ "web-components.action-panel",
911
+ TIME_SLIDER,
912
+ ],
913
+ evidence: ["test/web-components-rtl-remaining.test.ts"],
914
+ note: "The remaining web components render under dir=rtl with logical direction, spacing, alignment, and control-row styles, asserted with Arabic/RTL rendering coverage. The time slider goes further than style neutrality: its inline arrow keys are asserted to reverse under dir=rtl, and its thumb offset uses inset-inline-start with a mirrored transform.",
915
+ },
916
+ ],
917
+ failing: [
918
+ {
919
+ ids: allExcept(FEATURE_EDITOR, "controls.swipe-control", "controls.layer-list", "controls.basemap-switcher", "controls.legend", "web-components.map-status", "web-components.map", "web-components.layer-list", "web-components.legend", "web-components.feature-table", "web-components.chart", "web-components.search", "web-components.editor", "web-components.basemap-control", "web-components.bookmarks", "web-components.locate-control", "web-components.measure-control", "web-components.measurement", "web-components.sketch-control", "web-components.print-export", "web-components.action-panel", TIME_SLIDER),
920
+ note: "Styles use physical left/right offsets and margins rather than logical inline-start/inline-end properties, and no test renders any component under dir=rtl.",
921
+ },
922
+ ],
923
+ pendingNote: "No verdict rather than a known failure, which is the honest distinction for this component: unlike the rest of the kit its styles declare no physical left/right offsets at all, so it is not demonstrably RTL-hostile. It also uses no logical properties and nothing renders it under dir=rtl, so it is simply unverified.",
924
+ },
925
+ "visual-regression": {
926
+ passing: [
927
+ {
928
+ ids: [FEATURE_EDITOR],
929
+ evidence: ["test/playwright/web-components-basic.spec.mjs"],
930
+ note: "The browser fixture mounts the production-tier feature editor in its deterministic empty state and compares it with a committed Playwright baseline on every run, with animations disabled and the caret hidden.",
931
+ },
932
+ ],
933
+ pendingNote: "No committed reference screenshot exists for this component yet. The feature-table baseline is the first component visual comparison; remaining components stay pending until their fixtures have deterministic, reviewable baselines.",
934
+ },
935
+ "strict-csp": {
936
+ passing: [
937
+ {
938
+ ids: ALL_IDS,
939
+ evidence: ["test/playwright/web-components-csp.spec.mjs"],
940
+ note: "The browser catalog mounts every native and web component under a strict same-origin Content-Security-Policy with no style-src unsafe-inline, exercises representative rerenders, and asserts that no securitypolicyviolation events occur.",
941
+ },
942
+ ],
943
+ },
944
+ "zero-console-error": {
945
+ passing: [
946
+ {
947
+ ids: LIFECYCLE_HARNESS_IDS,
948
+ evidence: [LIFECYCLE_HARNESS],
949
+ note: "console.error and console.warn are spied for the whole mount / drive / re-render / unmount cycle and asserted never to be called.",
950
+ },
951
+ {
952
+ ids: [FEATURE_TABLE],
953
+ evidence: [LIFECYCLE_HARNESS],
954
+ note: "Covered for the controller lane only. The bounded engine lane is not driven by the harness and no feature-table suite spies on the console, so its query, paging, and conflict paths are unswept.",
955
+ },
956
+ {
957
+ ids: [
958
+ "web-components.map",
959
+ "web-components.editor",
960
+ "web-components.chart",
961
+ "web-components.basemap-control",
962
+ "web-components.bookmarks",
963
+ "web-components.locate-control",
964
+ "web-components.map-status",
965
+ "web-components.action-panel",
966
+ ],
967
+ evidence: ["test/web-components-accessibility.test.ts"],
968
+ note: "Each covered web component is mounted and disconnected while console.error and console.warn are spied; the complete lifecycle emits neither.",
969
+ },
970
+ {
971
+ ids: ["controls.basemap-switcher", "controls.swipe-control", "controls.legend", "controls.layer-list"],
972
+ evidence: ["test/controls/accessibility.test.ts"],
973
+ note: "Each native control is mounted and disconnected while console.error and console.warn are spied; the complete lifecycle emits neither.",
974
+ },
975
+ {
976
+ ids: ["web-components.feature-editor"],
977
+ evidence: ["test/web-components-feature-editor-element.test.ts"],
978
+ note: "The feature editor is mounted and disconnected while console.error and console.warn are spied; its complete lifecycle emits neither.",
979
+ },
980
+ {
981
+ ids: ["web-components.measurement"],
982
+ evidence: ["test/web-components-measurement-element.test.ts"],
983
+ note: "The measurement element is mounted and disconnected while console.error and console.warn are spied; its complete lifecycle emits neither.",
984
+ },
985
+ {
986
+ ids: [TIME_SLIDER],
987
+ evidence: [TIME_SLIDER_SUITE],
988
+ note: "The time slider is mounted, bound to a real playback controller, played, scrubbed, stepped, re-sped, and disconnected while console.error and console.warn are spied; the complete cycle emits neither, and the same sweep covers the unbound and degraded panels.",
989
+ },
990
+ ],
991
+ pendingNote: "Only uncaught-exception capture (Playwright pageerror) covers this component; the repo's console-error assertion helper is not applied to any component spec.",
992
+ },
993
+ "deterministic-disposal": {
994
+ passing: [
995
+ {
996
+ ids: LIFECYCLE_HARNESS_IDS,
997
+ evidence: [LIFECYCLE_HARNESS],
998
+ note: "Removing the element drops its controller state subscription and its root honua-controller-ready listener to zero, and a subsequent controller update reaches no detached element.",
999
+ },
1000
+ {
1001
+ ids: ["web-components.measurement"],
1002
+ evidence: ["test/web-components-measurement-element.test.ts"],
1003
+ note: "Disconnecting unbinds every map listener and restores double-click zoom.",
1004
+ },
1005
+ {
1006
+ ids: [TIME_SLIDER],
1007
+ evidence: [TIME_SLIDER_SUITE],
1008
+ note: "Disconnecting removes every controller subscription the element took (tick/play/pause/end), asserted by counting the controller's live listeners down to zero and by asserting a post-detach tick reaches no detached element; reconnecting re-takes exactly the same set once, and three detach/reattach cycles still leave one of each. Re-binding a second controller releases the first one's subscriptions before subscribing. The element owns no timer and no in-flight request; its pointer-drag listeners are released on pointerup and on disconnect.",
1009
+ },
1010
+ {
1011
+ ids: ["controls.legend", "controls.layer-list"],
1012
+ evidence: ["test/controls/legend.test.ts", "test/controls/layer-list.test.ts"],
1013
+ note: "The auto-refresh styledata subscription is dropped on disconnect and re-established on reconnect.",
1014
+ },
1015
+ {
1016
+ ids: ["controls.basemap-switcher"],
1017
+ evidence: ["test/controls/basemap-switcher.test.ts"],
1018
+ note: "Unbinding the map removes every layer and source the control added, and reconnecting restores the selection.",
1019
+ },
1020
+ {
1021
+ ids: ["controls.swipe-control"],
1022
+ evidence: ["test/controls/swipe-control.test.ts"],
1023
+ note: "Disconnecting during an active drag removes the pointermove, pointerup, and pointercancel listeners it installed.",
1024
+ },
1025
+ {
1026
+ ids: ["web-components.map"],
1027
+ evidence: [BROWSER_SPEC],
1028
+ note: "Removing the element tears down canvas, map, and runtime, asserted in a real browser. Unit-level coverage is not possible without a renderer.",
1029
+ },
1030
+ {
1031
+ ids: [FEATURE_TABLE],
1032
+ evidence: [FEATURE_TABLE_SUITE, LIFECYCLE_HARNESS],
1033
+ note: "Both lanes are covered: the controller subscription and root controller-ready listener (harness), and the bounded engine subscription — released on detach, re-taken exactly once on reconnect, with a re-assignment asserted not to double-subscribe and a reconnect asserted not to re-query. Its grid listeners are bound to nodes that the next innerHTML render replaces, so unlike the feature editor's former shadow-root binding they neither accumulate nor survive as a live handler. Still uncovered: the element does not cancel an in-flight engine refresh on disconnect (the engine is host-owned and cancels on its own dispose), and no test asserts that.",
1034
+ },
1035
+ {
1036
+ ids: [FEATURE_EDITOR],
1037
+ evidence: [FEATURE_EDITOR_SUITE],
1038
+ note: "Complete for what this element owns: the workflow subscription is released on disconnect and re-taken on reconnect, and the shadow-root keydown listener is released too — asserted by dispatching at the detached element's still-live shadow root and counting zero workflow calls, then exactly one after reconnect, and still exactly one after three detach/reattach cycles (issue #809). The element holds no timers and no AbortController of its own, so there is nothing further to release.",
1039
+ },
1040
+ ],
1041
+ pendingNote: "Nothing asserts what this component releases when it is disconnected.",
1042
+ },
1043
+ "duplicate-listener": {
1044
+ passing: [
1045
+ {
1046
+ ids: INTERACTIVE_HARNESS_IDS,
1047
+ evidence: [LIFECYCLE_HARNESS],
1048
+ note: "After ten controller-driven re-renders a single interaction still produces the same (non-zero) number of events it did after the first render, proving handlers are bound to freshly rendered nodes rather than accumulated on surviving ones.",
1049
+ },
1050
+ {
1051
+ ids: [FEATURE_TABLE],
1052
+ evidence: [LIFECYCLE_HARNESS, FEATURE_TABLE_SUITE],
1053
+ note: "Covered through the controller lane by the harness, and the bounded lane binds every grid handler to nodes the next render replaces rather than to the persistent shadow root, which is the arrangement that makes accumulation impossible. The bounded lane's scroll, cell-focus, and sort-button handlers are not themselves swept across repeated renders.",
1054
+ },
1055
+ {
1056
+ ids: [FEATURE_EDITOR],
1057
+ evidence: [FEATURE_EDITOR_SUITE],
1058
+ note: "One keystroke after four re-renders drives exactly one undo, one redo, and one cancel, asserted by counting workflow invocations rather than resulting state — which is the only way to see this defect class, since surplus undo()/cancel() calls are idempotent no-ops that leave state looking correct. The keydown listener is now bound once per connection on the shadow root instead of on every render (issue #809).",
1059
+ },
1060
+ {
1061
+ ids: ["web-components.measurement"],
1062
+ evidence: ["test/web-components-measurement-element.test.ts"],
1063
+ note: "Repeated mode rerenders leave exactly one map click and one double-click listener, proving handlers are not accumulated.",
1064
+ },
1065
+ {
1066
+ ids: [TIME_SLIDER],
1067
+ evidence: [TIME_SLIDER_SUITE],
1068
+ note: "After ten controller ticks and a messages reassignment, one click on the play/pause toggle drives exactly one controller call and emits exactly one honua-time-change, and one ArrowRight drives exactly one step — asserted by counting controller invocations rather than resulting state, which is the only way this defect class is visible on an idempotent transport API.",
1069
+ },
1070
+ {
1071
+ ids: ["controls.basemap-switcher"],
1072
+ evidence: ["test/controls/accessibility.test.ts"],
1073
+ note: "Repeated bases rerenders leave one click handler on the selected radio, evidenced by exactly one change event from one interaction.",
1074
+ },
1075
+ {
1076
+ ids: ["controls.swipe-control"],
1077
+ evidence: ["test/controls/swipe-control.test.ts"],
1078
+ note: "Repeated position rerenders leave one keyboard handler on the divider, evidenced by exactly one change event from one ArrowRight interaction.",
1079
+ },
1080
+ {
1081
+ ids: ["controls.layer-list"],
1082
+ evidence: ["test/controls/accessibility.test.ts"],
1083
+ note: "Repeated overlays rerenders leave one checkbox change handler, evidenced by exactly one change event from one click.",
1084
+ },
1085
+ {
1086
+ ids: ["web-components.map"],
1087
+ evidence: [BROWSER_SPEC],
1088
+ note: "After repeated controller-driven state updates, one zoom click produces the same viewport event count as the pre-update baseline in the real MapLibre browser fixture.",
1089
+ },
1090
+ ],
1091
+ notApplicable: [
1092
+ {
1093
+ ids: DISPLAY_ONLY_IDS,
1094
+ note: "A read-only presentation that binds no event handler; there is nothing that could accumulate. The harness asserts the absence of any interactive control, so adding one moves this row.",
1095
+ },
1096
+ {
1097
+ ids: CONTROLS_DISPLAY_ONLY_IDS,
1098
+ note: "The native legend binds no event handler because it is a read-only list presentation; there is nothing that could accumulate.",
1099
+ },
1100
+ ],
1101
+ pendingNote: "No test re-renders this component and asserts handler count. The existing bind/unbind assertions walk a collaborator's listener count 0 -> 1 -> 0, which is adjacent but does not catch double-add on repeated render.",
1102
+ },
1103
+ "memory-leak": {
1104
+ passing: [
1105
+ {
1106
+ ids: ALL_IDS,
1107
+ evidence: ["test/playwright/web-components-memory-leak.spec.mjs"],
1108
+ note: "Chromium's HeapProfiler.collectGarbage is used with a WeakRef for every catalog component; after removal and repeated forced collections, no detached element remains reachable.",
1109
+ },
1110
+ ],
1111
+ },
1112
+ "ssr-import": {
1113
+ passing: [
1114
+ {
1115
+ ids: ALL_IDS,
1116
+ evidence: [SSR_IMPORT_SUITE],
1117
+ note: "Both kit entrypoints, the element module, the catalog, and the registry are imported in a DOM-free Node environment with window and document asserted absent; no custom element is defined and no browser global is touched at module scope.",
1118
+ },
1119
+ ],
1120
+ },
1121
+ "secure-export": {
1122
+ passing: [
1123
+ {
1124
+ ids: ["web-components.print-export"],
1125
+ evidence: [
1126
+ "test/web-components-export-event-security.test.ts",
1127
+ "test/web-components-export-security.test.ts",
1128
+ "test/web-components-export-review2.test.ts",
1129
+ ],
1130
+ note: "The secure-export suites prove explicit-adapter success, fail-closed snapshot/state behavior, credential exclusion from state, bytes, filenames, errors, event detail, and visible status, plus idempotent release and cancellation.",
1131
+ },
1132
+ ],
1133
+ notApplicable: [
1134
+ {
1135
+ ids: allExcept("web-components.print-export"),
1136
+ note: "This component does not expose print, renderer snapshot, or sanitized state export actions; the secure export contract is owned by honua-print-export.",
1137
+ },
1138
+ ],
1139
+ },
1140
+ "bundle-budget": {
1141
+ passing: [
1142
+ {
1143
+ ids: ALL_IDS,
1144
+ evidence: ["bundle-budgets.json", "scripts/report-bundle-sizes.mjs"],
1145
+ note: "The owning kit's entrypoint (/controls or /web-components) carries a declared min+gzip ceiling enforced by npm run verify:bundle-budgets, with maplibre-gl, cesium, deck.gl, PDF/image encoders, and localization frameworks declared as forbidden static inputs (NFR-001).",
1146
+ },
1147
+ ],
1148
+ },
1149
+ };
1150
+ // ── expansion + queries ──────────────────────────────────────────────────
1151
+ function statusFor(gate, id) {
1152
+ const declaration = DECLARATIONS[gate];
1153
+ const definition = HONUA_COMPONENT_QUALIFICATION_GATES.find((entry) => entry.id === gate);
1154
+ if (!definition)
1155
+ throw new Error(`Unknown qualification gate "${gate}".`);
1156
+ const base = { gate, requirement: definition.requirement };
1157
+ for (const [status, groups] of [
1158
+ ["passing", declaration.passing],
1159
+ ["failing", declaration.failing],
1160
+ ["not-applicable", declaration.notApplicable],
1161
+ ]) {
1162
+ const matches = (groups ?? []).filter((group) => group.ids.includes(id));
1163
+ if (matches.length === 0)
1164
+ continue;
1165
+ const evidence = [...new Set(matches.flatMap((group) => group.evidence ?? []))].sort();
1166
+ const notes = matches.map((group) => group.note).filter((note) => Boolean(note));
1167
+ return {
1168
+ ...base,
1169
+ status,
1170
+ evidence,
1171
+ ...(notes.length > 0 ? { note: notes.join(" ") } : {}),
1172
+ };
1173
+ }
1174
+ return {
1175
+ ...base,
1176
+ status: "pending",
1177
+ evidence: [],
1178
+ ...(declaration.pendingNote ? { note: declaration.pendingNote } : {}),
1179
+ };
1180
+ }
1181
+ /** The full matrix row for one catalog id, or `undefined` for an unknown id. */
1182
+ export function getComponentQualification(id) {
1183
+ const entry = HONUA_COMPONENT_CATALOG.find((candidate) => candidate.id === id);
1184
+ if (!entry)
1185
+ return undefined;
1186
+ return {
1187
+ id: entry.id,
1188
+ tag: entry.tag,
1189
+ source: entry.source,
1190
+ gates: HONUA_COMPONENT_QUALIFICATION_GATES.map((gate) => statusFor(gate.id, entry.id)),
1191
+ };
1192
+ }
1193
+ /** The full matrix, in catalog order. */
1194
+ export function listComponentQualifications() {
1195
+ return HONUA_COMPONENT_CATALOG.map((entry) => {
1196
+ const qualification = getComponentQualification(entry.id);
1197
+ if (!qualification)
1198
+ throw new Error(`Catalog entry "${entry.id}" has no qualification row.`);
1199
+ return qualification;
1200
+ });
1201
+ }
1202
+ /** One gate's definition. */
1203
+ export function describeComponentQualificationGate(gate) {
1204
+ return HONUA_COMPONENT_QUALIFICATION_GATES.find((entry) => entry.id === gate);
1205
+ }
1206
+ /** Counts and remaining blockers for one row, or for the whole matrix when `id` is omitted. */
1207
+ export function summarizeComponentQualification(id) {
1208
+ const rows = id ? [getComponentQualification(id)].filter((row) => row !== undefined) : listComponentQualifications();
1209
+ const cells = rows.flatMap((row) => row.gates);
1210
+ const blocking = new Set();
1211
+ for (const cell of cells) {
1212
+ if (cell.status === "failing" || cell.status === "pending")
1213
+ blocking.add(cell.gate);
1214
+ }
1215
+ return {
1216
+ passing: cells.filter((cell) => cell.status === "passing").length,
1217
+ failing: cells.filter((cell) => cell.status === "failing").length,
1218
+ pending: cells.filter((cell) => cell.status === "pending").length,
1219
+ notApplicable: cells.filter((cell) => cell.status === "not-applicable").length,
1220
+ blocking: [...blocking].sort(),
1221
+ };
1222
+ }
1223
+ /**
1224
+ * Whether a component has cleared **every** gate in this matrix.
1225
+ *
1226
+ * `not-applicable` cells count as cleared (that is what the status means, and
1227
+ * why it requires a justifying note); `pending` and `failing` do not.
1228
+ *
1229
+ * This is deliberately *not* the same claim as the catalog's
1230
+ * `supportTier: "production-tier"`, and conflating the two would misreport both.
1231
+ * The catalog tier is a **functional maturity** claim owned by whichever feature
1232
+ * issue raised the component (#680-#683): `<honua-feature-editor>` is
1233
+ * production-tier because #680 delivered capability-aware editing, conflict
1234
+ * handling, snapping, and attachment staging on it. This function is the
1235
+ * **cross-cutting gate** axis, which no component has cleared yet.
1236
+ *
1237
+ * `scripts/component-qualification.mjs` therefore enforces only the direction
1238
+ * that is actually implied: a component that has cleared every gate must be
1239
+ * declared production-tier, because gate completion is strictly the harder bar.
1240
+ * The reverse does not hold, so a production-tier component simply carries its
1241
+ * still-open gates in the manifest's `openGates` — visible, counted, and
1242
+ * impossible to mistake for done. See {@link listOpenQualificationGates}.
1243
+ */
1244
+ export function isComponentProductionQualified(id) {
1245
+ const qualification = getComponentQualification(id);
1246
+ if (!qualification)
1247
+ return false;
1248
+ return qualification.gates.every((cell) => cell.status === "passing" || cell.status === "not-applicable");
1249
+ }
1250
+ /**
1251
+ * The gates a component has not yet cleared (`pending` or `failing`), in gate
1252
+ * order. Empty exactly when {@link isComponentProductionQualified} is true.
1253
+ *
1254
+ * This is what keeps a `production-tier` catalog claim honest: the tier says the
1255
+ * component's *feature* work landed, and this says precisely what remains on the
1256
+ * accessibility, localization, visual, CSP, lifecycle, and bundle axes.
1257
+ */
1258
+ export function listOpenQualificationGates(id) {
1259
+ const qualification = getComponentQualification(id);
1260
+ if (!qualification)
1261
+ return [];
1262
+ return qualification.gates
1263
+ .filter((cell) => cell.status === "pending" || cell.status === "failing")
1264
+ .map((cell) => cell.gate);
1265
+ }