ranger-compiler 3.5.1 → 3.5.2

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 (338) hide show
  1. package/CHANGELOG.md +1064 -20
  2. package/LICENSE +3 -1
  3. package/README.md +123 -28
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79923 -56921
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. package/dist/package.json +0 -47
package/CHANGELOG.md CHANGED
@@ -7,6 +7,1050 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Removed
11
+
12
+ - **Erazer moved to its own repository,
13
+ [terotests/Erazer](https://github.com/terotests/Erazer).** `gallery/erazer`,
14
+ its `erazer*` npm scripts, the CI layout-lab step, and the `/evg/erazer/`
15
+ page and its checks in the Pages deploy are gone from this repository.
16
+ The compiled `gallery/erazer/bin/*.js` that the removal left tracked are
17
+ deleted too.
18
+ - **The EVG live-build harness moved to its own repository,
19
+ [terotests/EvgHarness](https://github.com/terotests/EvgHarness).**
20
+ `gallery/evg/livebuild` and its `livebuild*` npm scripts are gone. The
21
+ harness clones Ranger and Erazer (or uses `RANGER_DIR` / `ERAZER_DIR`) and
22
+ links itself back in at `gallery/evg/livebuild`; both link paths are
23
+ ignored here.
24
+
25
+ ### Fixed
26
+
27
+ - **`npm run test:publish` depended on the machine it ran on.** The
28
+ interpreter's local time is UTC, and `runtime-conformance` compared it with
29
+ Node formatting in the host's zone, so the `Intl` date probes failed
30
+ anywhere but UTC. `tests/vitest.config.ts` sets `TZ=UTC` for every fork.
31
+ `docs-usage` counted every `.rgr` file on disk, so test output, self-host
32
+ copies of `lib/` and files a moved-out project left behind could mark a
33
+ legacy library as used; it now reads `git ls-files`.
34
+ - **`npm run build:dist:module` failed with 140 TypeScript errors.** The
35
+ TypeScript writer typed `buffer` as `Uint8Array`, but the es6 buffer
36
+ templates make an `ArrayBuffer` with a `DataView` in `_view`. `buffer` is
37
+ now `(ArrayBuffer & { _view: DataView })` on TypeScript, and the
38
+ templates attach `_view` with `Object.assign`, which gives that type.
39
+ The generated JavaScript behaves as before.
40
+ - **`npm run test:publish` passes again.** `tests/http-server.test.ts` and
41
+ its fixture are removed: they ran the es6 HTTP server that the plugin
42
+ removal took out (`start` / `stop` and `RangerJavaScriptHttpServerWriter`).
43
+ The C++ ownership test accepts `h->item.value()` for an optional field,
44
+ and the Dart self-host test checks for `List<dynamic>` in place of
45
+ `jsonEncode`, which the compiler no longer calls.
46
+ - **A document's `theme` now turns on its `.theme-<name> .class` rules.**
47
+ `EVGLayout` applied a document's stylesheet with an empty theme, so every
48
+ theme-scoped rule was dead in the live page, `measure` and every other
49
+ layout path: `{"theme":"dark"}` on the root parsed, round-tripped, and
50
+ styled nothing. The UI kit's `.theme-dark` rules for every piece now
51
+ apply. `evg:json:test` covers it.
52
+ - **UI kit `bars`: the bars sit in their row.** `.ui-bar-col` had
53
+ `justify-content: flex-end`, and a content-sized column with that laid its
54
+ bar and label out one column-height above itself, over the card's title.
55
+ `measure` did not report it, because nothing clips there.
56
+
57
+ ### Added
58
+
59
+ - **Dashboard pieces in the UI kit.** `ui_kit.mjs add` builds `tabbar`,
60
+ `pills`, `tiles`, `bars` and `banner` as whole pieces with their rules in
61
+ `gallery/ui/theme/base.css`, beside `row`, `card`, `appbar`, `chips` and
62
+ `field`.
63
+
64
+ - **Native generic classes on Rust.** A generic class whose body only stores,
65
+ moves and returns its parameter values is written as `struct History<Op>`
66
+ with `impl<Op: Clone> History<Op>`, and used as `History<i64>` /
67
+ `History::<i64>::new()`. A parameter typed by the type parameter is taken by
68
+ value (`op: Op`) and the call sites pass owned values. A class that extends,
69
+ is extended or `does` a trait, a method that needs its own handle, and a
70
+ template named like a Rust prelude type (`Box`, `Vec`, …) keep the copies.
71
+
72
+ - **Native generic classes.** A `class History @params(Op)` whose body only
73
+ stores, moves and returns its parameter values is written once, as a
74
+ generic class of the target: `template <class Op> class History` on C++,
75
+ `class History<Op>` on Java, C#, Kotlin, Dart and TypeScript,
76
+ `class History[Op]` on Scala, `type History[Op any] struct` on Go,
77
+ `class History(Generic[Op])` on Python, and one `History` class on
78
+ JavaScript and PHP. Uses are spelled `History<int>`, `*History[int64]` and
79
+ so on. The type checker still checks one class per argument list. A body
80
+ that needs to know its parameter, and every generic class on Rust, Swift
81
+ and LLVM, keeps the copies (`History_int`). `-generics-report` prints the
82
+ choice, `-no-native-generics` turns it off.
83
+
84
+ - **`char_length`: how many characters, on all fourteen targets.** `strlen`
85
+ counts the target's own unit — UTF-16 code units on JavaScript, Java,
86
+ Kotlin, C#, Scala and Swift, UTF-8 bytes on Rust, Go, C++ and PHP, code
87
+ points on Python. That is the right number for a scan and the wrong one for
88
+ anything a person sees: a column, a width, a padding count. `char_length`
89
+ is the count that does not move: `len(s)` on Python, `chars().count()` on
90
+ Rust, `utf8.RuneCountInString` on Go, `codePointCount` on the JVM,
91
+ `runes.length` on Dart, `mb_strlen` on PHP, `unicodeScalars.count` on
92
+ Swift, and a non-continuation-byte or non-low-surrogate count where the
93
+ standard library has nothing. It is the length of `(to_chars s)` without
94
+ building the array, and for ASCII it is `strlen`.
95
+
96
+ - **EVG live build exports one Ranger UI JSON document.** Save keeps a
97
+ design on this machine. **Export** copies a single `.ranger.json`
98
+ (`format: ranger-ui`, version 2): a semantic `ui` tree, a CSS string,
99
+ the machine, and a `components` manifest. Known controls collapse to
100
+ `rave.Switch` / `SettingsRow` rather than a track and a thumb.
101
+ Compact is the clipboard; Full adds `compiled.evg` and layout debug.
102
+ `npm run livebuild:export`.
103
+
104
+ ### Changed
105
+
106
+ - **The Rust header is built from what the file contains.** Every output used
107
+ to open with the same block — `#![allow(unused_parens)]`,
108
+ `unused_mut`, `unused_variables`, `unused_assignments`, `dead_code` and
109
+ three clippy lines — whether or not the file held the shape. An allow
110
+ nobody needs hides the backend's own regressions: a stray `mut` or a
111
+ redundant paren the generator starts emitting is swallowed by the pragma
112
+ instead of being reported. Each line is asked for now: `unused_mut` by the
113
+ one `let mut` the writer could not prove, the rest by predicates over the
114
+ program (a parameter the body never reads, a dead store, a name whose
115
+ snake_case spelling is already another name here, an `if` inside an `if`,
116
+ seven parameters, a borrowed `Vec`). Across the twelve `friendly` studies
117
+ the header matches what each file needs exactly, and no study asks for a
118
+ line it does not need. Only `dead_code` is unconditional — a
119
+ program's public surface is dead code in a single-file rendering of it,
120
+ which says nothing about the generator. The predicates skip what the
121
+ class-writing loop skips: `Vector.set` has an unused parameter and is never
122
+ emitted, and it alone was asking every file for `unused_variables`.
123
+
124
+ - **`unused_parens` is not asked for either, because the parentheses are
125
+ gone.** A `let` value, an assignment's right side and a template's argument
126
+ slot all delimit the expression, so the pair an operator template puts
127
+ around its whole result comes off there: `let n: i64 = (xs.len() as i64);`
128
+ and `rg_substring(&s, i, (i + 1))` were the two shapes, 996 rustc warnings
129
+ between them. An operand with an operator on either side keeps its pair —
130
+ that is what makes the precedence right. The template-slot rule is
131
+ target-neutral, so `g.check(0 - 1)` is what every target emits now.
132
+
133
+ - **`mut` is asked of the body rather than of the type.** An object local
134
+ used to be `mut` whatever was done with it, and so did a collection, a
135
+ buffer and every parameter binding. An object local now takes `mut` only
136
+ when the body assigns it, writes through it, calls a method emitted
137
+ `&mut self` on it, or hands it where the callee takes `&mut`; a parameter
138
+ takes it only when the body reassigns the parameter itself. Collections and
139
+ `&mut` parameters keep the blanket `mut` and ask for the allow — dropping
140
+ it on those was measured at 43 and 11 rustc errors, every one an E0596
141
+ where a call site writes `&mut name` and the callee is invisible from
142
+ there.
143
+
144
+ - **Three Rust shapes from the same reading.** A folded object literal binds
145
+ `let line: CartLine = …` rather than `let mut`, unless the body writes
146
+ through the name after the fold — and the local is renamed where it
147
+ collided (`g` → `g_1`), so the scan matches the source name and the
148
+ compiled one. A `for` body that only calls `&self` methods on the element
149
+ binds `&T`: `for line in &self.lines`, not `.iter().cloned()`; the answer
150
+ is `rust_mut_self`, which is settled for every class before the first line
151
+ is written. And a constructor names `Self { … }` and gives an empty owned
152
+ String `String::new()`.
153
+
154
+ Measured on the Rust rendering of this compiler: **2 148 warnings → 321**,
155
+ with 0 rustc errors, and the rendering still compiles the compiler to
156
+ output byte-identical to `dist/rgrc.js`. What is left was never covered by
157
+ any of these allows. A shopping-cart program of the kind the playground
158
+ compiles now carries `#![allow(dead_code)]` alone and draws no rustc
159
+ warning at all; `tests/codegen-rust.test.ts` keeps it that way.
160
+
161
+ - **The front-page hero copy is the shorter two columns.** Silver Bullet:
162
+ Ranger is a little heavier to get started with, and a golden test when
163
+ you have more than one language to target. Stay DRY: AI makes generating
164
+ code easier; consistent validation across platforms is the remaining
165
+ problem.
166
+
167
+ - **Front-page bench re-run: Go is 236 ms, third after C++ and Rust.**
168
+ Re-running after the string-index fix (C++ 136, Rust 170, Go 236, Java
169
+ 351, JavaScript 508, Python 971). PHP, C# and Kotlin were not on this
170
+ machine this run. The Platforms and Gallery sections come off the front
171
+ page: they are AGPL gallery work, and the Platforms copy was too specific
172
+ for the language page.
173
+
174
+ - **`-strict-strings` asks whether the unit is observable, and the compiler
175
+ now reports zero.** The first version listed every index whose subject was
176
+ not an ASCII literal — 322 sites, which named the language rather than the
177
+ defect. A scan is not a defect: `while (i < (strlen s)) { charAt s i }`
178
+ reads the same characters on a byte target and a UTF-16 one; only the
179
+ numbers differ, and the program never sees them.
180
+
181
+ The flag now reports the three ways a number escapes — a `strlen` that
182
+ indexes nothing, a `to_chars` offset handed to `charAt`, a `charcode` on
183
+ something that is not an ASCII literal — and proves the rest quiet: an
184
+ emptiness test, a length that indexes some string, a length that bounds an
185
+ index, two counts of the same string compared with each other. A length
186
+ against a constant or against another string's length is a **note**, not a
187
+ finding. `strlen` is covered, which it was not before, and that is where
188
+ the real defects were.
189
+
190
+ Four of them, all a count of characters someone sees: the CLI progress bar
191
+ padded to a different column depending on which build of the compiler wrote
192
+ it; `formatSource` wrapped the same file in different places, because a
193
+ comment holding an em dash was one column wide under Node and three in the
194
+ Rust and Go self-hosts; `columnNumber`, which goes into errors and into the
195
+ source map; and `(cc N)`, which burns a character code into generated
196
+ source and so must not depend on its host. Three escapers now `switch` on
197
+ the one-character slice instead of on `charcode` of it.
198
+
199
+ Where provenance crosses a function boundary — a scan position kept in a
200
+ field, a length arriving as a parameter — the source says so with
201
+ `@(units)` on the `def` or on the function, so the claim is visible where
202
+ it is made. `tests/strict-strings.test.ts` keeps the count at zero.
203
+
204
+ ### Fixed
205
+
206
+ - **A string literal holding BOTH an escape and a non-ASCII character came
207
+ out broken.** `"merkintä... esim. \"treeni\""` compiled to
208
+ `merkint\uFFFD\uFFFD...`: two replacement characters where the `ä` was.
209
+ The parser's escape-decoding path converted the literal to a `charbuffer`
210
+ and copied it back one unit at a time, and since a `charbuffer` is UTF-8
211
+ bytes on every target now, each byte of a multi-byte character was decoded
212
+ by itself. It reads the string directly — `strlen`, `charAt` and
213
+ `substring` index the same unit as each other on any one target, and every
214
+ character the loop looks at (`\`, `"`, `n`, …) is ASCII. The same shape as
215
+ the `EncodeString` fix in the six writers, in the one place that was
216
+ missed; the checked-in `dist/rgrc.js` carried the damage in one of its own
217
+ messages and needed two bootstrap passes to converge.
218
+
219
+ - **A Rust `switch` over strings broke on a quote, a backslash or a
220
+ newline.** A `match` arm is a pattern, so the Rust writer wrote the case
221
+ literal with `(str N)` — the text and nothing else, because the
222
+ `.to_string()` a literal gets in expression position is not something a
223
+ pattern takes. Unescaped, `case "\""` came out as `"""` and rustc read
224
+ three tokens where one was meant; `case "\n"` put a real newline inside
225
+ the pattern. `(estr N)` is the same accessor with the target's own string
226
+ escaping applied and still no quotes of its own, and the Rust `case`
227
+ template uses it. Found by moving three escapers onto a string `switch`.
228
+
229
+ - **The Rust rendering of the compiler compiles the compiler.** It had type-
230
+ checked with zero rustc errors for years and aborted on the first file it
231
+ was ever given, because `RefCell` checks at run time and the self-host gate
232
+ only type-checked. Two causes, both the same shape — a borrow that outlives
233
+ the statement that took it:
234
+
235
+ A `for` head is the one place Rust keeps a temporary alive across a block,
236
+ so a collection reached through a field kept that object borrowed while the
237
+ body ran and the first `borrow_mut` panicked. And a trait method with a
238
+ `&mut self` receiver means the call site holds a `RefMut` for the whole
239
+ call — while the compiler and its writers are mutually recursive by design.
240
+
241
+ A trait family that can be re-entered through its own handle is now
242
+ implemented for `Rc<RefCell<C>>` rather than for `C`, so `&self` IS the
243
+ handle and dispatching borrows nothing. Which families those are is
244
+ answered from the field graph rather than declared: the trait root holds a
245
+ field of some class X, X holds a field of the family's type, and both call
246
+ through it.
247
+
248
+ `npm run selfhost:run:rust` is the new gate and holds Rust to what C++ and
249
+ Go already meet: build it, make it compile `compiler/Compiler.rgr`, and
250
+ diff the result against `dist/rgrc.js`. It is identical, all 5 596 785
251
+ bytes, in 7.3 s against the node host's 7.8 s.
252
+ `docs/plans/PLAN_RUST_REENTRANCY.md` is the write-up.
253
+
254
+ - **Rust and Go index the byte their string is made of.** `strlen`, `charAt`
255
+ and `substring` on both counted characters, which cost them the quadratic
256
+ scan — `charAt` walked from the start on every read, and on Go `[]rune(s)`
257
+ copied the whole string each time. `gallery/friendly/bench/strscan.rgr` at
258
+ 120 000 characters: Rust 8 830 ms → 11 ms, which is C++ to the millisecond;
259
+ Go did not finish inside two minutes → 19 ms.
260
+
261
+ It was also a correctness fix. `indexOf` is `strings.Index` on Go and
262
+ answers a BYTE offset, so a scanner that found a delimiter and sliced at it
263
+ sliced in the wrong place as soon as anything non-ASCII stood before it:
264
+ for `"ä,b"` the head came back as `"ä,"` and the tail as `""` — the rest of
265
+ the text, dropped without a word. Rust had the same bug and paid an O(n)
266
+ `chars().count()` on every `indexOf` to hide it; that conversion is gone.
267
+ Rust's `charcode` read `as_bytes()[0]` all along and so disagreed with its
268
+ own `charAt`; Java's read `getBytes()[0]` and answered −61 where `charAt`
269
+ said 228. All nine runnable targets are now internally consistent.
270
+
271
+ - **A byte-hosted compiler wrote every non-ASCII literal twice encoded.**
272
+ The C++ self-host emitted `"a—b"` into its JavaScript output as
273
+ `C3 A2 C2 80 C2 94` instead of `E2 80 94`: `EncodeString` rebuilt each
274
+ character with `strfromcode`, which writes a code point, from what `charAt`
275
+ gave it, which on a byte host is a byte. True of C++ and PHP all along and
276
+ never noticed. A one-unit `substring` copies the unit across instead, and
277
+ the C++ and Go self-hosts now produce output byte-identical to the
278
+ node-hosted compiler's.
279
+
280
+ - **`to_chars` is the portable indexable view of text.** `def cs:[int]
281
+ (to_chars s)` gives Unicode code points, the same sequence on every target,
282
+ built once in O(n) and read in O(1). `charAt` on a `string` stays the
283
+ target's own unit — that is what makes it O(1) there — and is right for a
284
+ scanner over ASCII structure; `to_chars` is for text a human wrote, where
285
+ `"a😀b"` has to be three characters and not two UTF-16 units plus two.
286
+ Because the program names the conversion, the allocation is asked for
287
+ rather than hidden behind an index.
288
+
289
+ - **A `charbuffer` is a buffer of bytes, and `to_charbuffer` is a string's
290
+ UTF-8.** One element is one octet, 0..255, not a character; UTF-8 belongs
291
+ to `to_charbuffer` and `to_string`, the two operators that cross between
292
+ text and bytes, because a conversion cannot be made without naming an
293
+ encoding. The buffer itself is just bytes — one holding a JPEG is not
294
+ "UTF-8 bytes".
295
+
296
+ It used to be whatever the host's string happened to be made of: UTF-16
297
+ units on JavaScript, Kotlin and Dart, code points on Python, bytes on the
298
+ other eight, and on Scala a `toByte` cast that truncated anything above
299
+ U+00FF. `to_charbuffer` is the explicit conversion — the program asks for
300
+ the byte view by name — so it is the one place a single portable unit can
301
+ be promised, and now it is: `"a—b"` is five bytes on all of them.
302
+
303
+ Measuring it with `tests/fixtures/charbuffer_units.rgr` turned up three
304
+ holes as well: `to_string` on a charbuffer did not compile on Rust, Java or
305
+ Kotlin, `charAt` on one returned a *signed* byte on the JVM targets, and
306
+ Swift 6 had no `to_charbuffer` template at all. Java's conversion used the
307
+ platform default charset rather than UTF-8.
308
+
309
+ A `charbuffer` is `Uint8Array` on JavaScript and TypeScript, `bytes` on
310
+ Python and `ByteArray` on Kotlin. `RangerLispParser` holds its source in
311
+ one, so the JavaScript self-host now scans the same bytes the C++ one does,
312
+ at the same speed. `tests/charbuffer-units.test.ts` asserts the agreement;
313
+ `docs/plans/PLAN_STRING_INDEXING.md` is the plan this is stage 1 of.
314
+
315
+ - **What a `string` index means is now measured and written down.**
316
+ `strlen`, `charAt` and `substring` mean a UTF-16 code unit on six targets, a
317
+ Unicode code point on three and a UTF-8 byte on two, and nothing said so.
318
+ `tests/fixtures/string_units.rgr` and `tests/string-units.test.ts` pin each
319
+ target's answer, `gallery/friendly/bench/strscan.rgr` measures the other
320
+ half — the ordinary index scan is O(n²) on Rust and Go — and `ai/QUICKREF.md`
321
+ says both where `string` is documented. No behaviour changed; the defect is
322
+ visible now.
323
+
324
+ - **Generated-code quality is three questions now, not one ranking.** The
325
+ single ordering read as a verdict, and it was one reading of the generated
326
+ files. It is split into correctness, speed and idiom, because a target can
327
+ do well on one and badly on another.
328
+
329
+ *Correctness* is what `gallery/friendly/compile.sh` already measured: eight
330
+ targets agree on all twelve studies. The one place they do not is integer
331
+ width — `100000 * 100000` answers `10000000000` on JavaScript, Python, PHP,
332
+ Go and Rust and `1410065408` on C++, C#, Java and Kotlin, and on C++ the
333
+ overflow is undefined behaviour rather than a wrap.
334
+ `gallery/friendly/bench/intwidth.rgr` is the probe.
335
+
336
+ *Speed* is new: `gallery/friendly/bench/` is the same Ranger program, five
337
+ kernels, each timing itself with `wall_clock_ms`. Kernels only: C++ 260 ms,
338
+ Kotlin 466, C# 663, Java 760, PHP 801, JavaScript 1076, Rust 2556,
339
+ Python 2566, Go 143882. (Re-measured after the string-indexing work: C++
340
+ 265, Rust 397, Kotlin 485, C# 684, Go 694, Java 753, PHP 786,
341
+ JavaScript 1040, Python 2607.) Two of those are the compiler's doing and are
342
+ written down — `charAt` is O(n) on Go and Rust, and a Ranger map is a plain
343
+ object on JavaScript.
344
+
345
+ *Idiom* is the score that stays on the front page, and it is measured
346
+ against a published twelve-check table in `gallery/friendly/README.md` so
347
+ each cell can be disputed against the file it came from. On this compiler,
348
+ after native enums on twelve targets, Python annotations, PHP typed
349
+ properties and the ownership work: Swift 92, Kotlin 88, TypeScript 83,
350
+ Dart 83, C++ 83, C# 79, Scala 79, Python 75, Rust 75, JavaScript 71,
351
+ PHP 71, Java 67, Go 67. This supersedes the #1024 rescoring below. Swift is
352
+ top and has never been compiled on the build machine, which is exactly why
353
+ the three axes are kept apart.
354
+
355
+ - **`wall_clock_ms` works on PHP, Dart, Scala and Swift.** They fell through
356
+ to the `*` template, which is `0.0`, so every duration a program measured
357
+ on those targets was zero.
358
+
359
+ - **The front-page hero columns are gold, not a magic runtime.** The first
360
+ column is still “There is no Silver Bullet.” Ranger is more like gold:
361
+ heavier to start with, and a golden-file test when you target more than
362
+ one language. The second column is “Stay Dry. Stay Foolish.” — sharing
363
+ code across platforms is half the problem; AI does not provide the
364
+ consistent validation, and Ranger does.
365
+
366
+ - **C++: a `record` nothing aliases is a value, not a `shared_ptr`.**
367
+ `StaticAnalyzer.analyzeClassSharing` already walks the whole program and
368
+ decides which classes are aliased and held; the pass already ran for C++
369
+ compilations; only the Rust writer read the answer. So the same study came
370
+ out as `struct Point` + `fn manhattan(p: &Point)` on Rust and
371
+ `class Point` + `int manhattan(const std::shared_ptr<Point>&)` on C++.
372
+ A record that verdict clears is now a value everywhere it appears — the
373
+ parameter is `const Point&`, `new Point(3 4)` is `Point(3, 4)` on the stack,
374
+ and the copying builder in study 08 returns a `Request`. The C++-specific
375
+ disqualifiers live beside it in
376
+ [`compiler/CppValueAnalysis.rgr`](compiler/CppValueAnalysis.rgr): an
377
+ `@(optional)` of the record (an optional object *is* the null pointer on this
378
+ target), a `cast` to it, a method that hands out bare `this`
379
+ (`shared_from_this()` needs the pointer), a field of its own type,
380
+ inheritance in either direction, and membership of a closed family, which
381
+ keeps the family's own rule. `-strict-ownership` prints
382
+ `ownership[cpp] class Point -> value` or the reason it did not;
383
+ `-cpp-shared-classes` restores the old lowering. Classes with behaviour are
384
+ the same question and wait for `const` member functions: a `const User&`
385
+ cannot call `who.label()` until the writer emits one.
386
+
387
+ Two things the value form needed that the pointer form never did. A
388
+ parameter the body WRITES THROUGH is `Point&`, not `const Point&`: behind a
389
+ `shared_ptr` the `const` was on the pointer and `p->x = 1` compiled, and on
390
+ a value it is on the object. `StaticAnalyzer.markVarAsMutated` has been
391
+ recording that fact as `needs_cpp_reference` all along; the writer suppressed
392
+ the `&` for class types because a `shared_ptr` never needed one. And a `T&`
393
+ does not bind a temporary, so `m.bump((new Point(1 2)))` goes through
394
+ `rg_arg_ref`, which is what that helper already existed for.
395
+
396
+ - **Every target's `for` is now that target's own loop.** The question is one
397
+ question -- can this loop be written over the collection instead of over an
398
+ index? -- so it is answered once, in
399
+ [`compiler/ForLoopAnalysis.rgr`](compiler/ForLoopAnalysis.rgr), and each
400
+ writer spells the answer its own way:
401
+
402
+ | target | was | is |
403
+ | --- | --- | --- |
404
+ | JavaScript / TypeScript | `for ( let i = 0; i < xs.length; i++)` | `for ( const v of xs)` |
405
+ | Python | `for i, v in enumerate(xs):` | `for v in xs:` |
406
+ | Go | `var i int64 = 0; for ; i < int64(len(xs)) ; i++` | `for _, v := range xs {` |
407
+ | Java | `for ( int i = 0; i < xs.size(); i++)` | `for ( Integer v : xs)` |
408
+ | Kotlin | `for ( i in xs.indices )` | `for ( v in xs )` |
409
+ | C# | `for ( int i = 0; i < xs.Count; i++)` | `foreach ( int v in xs)` |
410
+ | Dart | `for ( int i = 0; i < xs.length; i++)` | `for ( final v in xs)` |
411
+ | Swift | `for (_, v) in xs.enumerated()` | `for v in xs` |
412
+ | C++ | `for ( int i = 0; i != (int)(xs.size()); i++)` | `for ( int v : xs )` |
413
+
414
+ Two conditions, and both are safety rather than length: the body must not
415
+ read the index, and must not touch any NAME the collection expression rests
416
+ on. A foreach form takes one iterator, enumerator or borrow for the whole
417
+ loop, so a body that appends walks something the index form does not -- an
418
+ invalidated iterator on C++, a `ConcurrentModificationException` on Java and
419
+ Kotlin, an `InvalidOperationException` on C#, a different answer on Go and
420
+ Swift. Every target keeps the index loop there, and `tests/loops-native.test.ts`
421
+ asserts both halves on all ten. Rust already made this decision
422
+ (PLAN_RUST_SEMANTIC_IDIOMS K) and es5 is deliberately left out, because
423
+ `for...of` is ES6.
424
+
425
+ - **C++: `for ( int v : xs )`.** The generated `for` was always
426
+ `for ( int i = 0; i != (int)(xs.size()); i++) { int v = xs.at(i); … }`. When
427
+ the body neither reads the index nor touches any name the collection rests
428
+ on, it is a range-`for` now — a scalar element by value, everything else
429
+ `const T&`. Both conditions are what makes it safe rather than merely
430
+ shorter: a range-`for` holds one iterator pair for the whole loop, so a body
431
+ that pushes would walk invalidated iterators where the index form re-reads
432
+ `.size()` every turn. Same decision the Rust writer makes
433
+ (PLAN_RUST_SEMANTIC_IDIOMS K), and deliberately not an
434
+ `<algorithm>` / `<ranges>` lowering.
435
+
436
+ - **C++ and Rust: the preamble goes in only when the program reaches it.**
437
+ The ordered-map preamble was gated already; these were not. On C++,
438
+ `template <class T> class r_optional_union` went into every file that had a
439
+ union — and `Any`, the compiler's own union of every declared class, is one
440
+ the parser declares for every program, so the answer was always yes. `Any`
441
+ and its forward declarations now go in only when the program says `Any`,
442
+ `r_optional_union` only when some `@(optional)` names a closed family, and
443
+ `rg_arg_ref` only when a call site needs it. The twelve gallery studies lost
444
+ 291 lines; study 07 is 67 lines, from 88. On Rust, `use std::rc::Rc`,
445
+ `use std::cell::RefCell` and the `RgAnyRef` / `rg_downcast` / `RgIdentical`
446
+ trio go in only when the cell can reach the output — a shared class, a
447
+ `@(weak)` field, a closed family, a behaviour-only trait used as a type, or
448
+ an inheritance family. Six of the twelve Rust studies have none of those and
449
+ drop all thirteen lines.
450
+
451
+ Gate for all three: `bash gallery/friendly/compile.sh` (twelve studies on
452
+ every target with a toolchain, every `attempts/` file still refused, six
453
+ targets agreeing on all twelve outputs), the C++ selfhost build, and
454
+ `scripts/rust-selfhost-check.sh`. The plan is
455
+ [`docs/plans/PLAN_CPP_IDIOMS.md`](docs/plans/PLAN_CPP_IDIOMS.md).
456
+
457
+ ### Added
458
+
459
+ - **A smoke effect**, `evg-surface-effect: smoke`, and three presets for it.
460
+ What makes smoke read as smoke is that it curls, and curls at every size at
461
+ once, so the field is DOMAIN WARPED — fbm evaluated at a point two other fbms
462
+ have already moved. One warp gives the billows, the second the tendrils that
463
+ come off their edges. The bar the field has to clear rises with height, which
464
+ is what leaves a full floor, wisps above it and black over those rather than
465
+ a fog that fades out evenly; `evg-fx-height` moves that line, and at 3 or
466
+ more the same effect is a cloud filling the box. It is lit by comparing the
467
+ field with itself one step toward `evg-fx-angle` — a gradient, and what gives
468
+ a cloud its volume. `.fx-stage`, `.fx-cloud` and `.fx-haze` are in
469
+ `effect-presets.css`, so they are in the gallery demo's background picker and
470
+ on its contact sheet; the pixel gate holds the effect to banking up along its
471
+ own floor, thinning toward the top, filling the box when told to, and moving.
472
+
473
+ - **A background picker on the published effects demo**
474
+ ([`/ui/demo/?demo=effects`](https://terotests.github.io/Ranger/ui/demo/?demo=effects)).
475
+ The eleven blocks of `lib/evg/gl/effect-presets.css` are in the rail, and
476
+ picking one TYPES it into the live stylesheet under the canvas — the box's
477
+ own declarations kept, the effect's replaced — after which the cascade reads
478
+ it like any other edit. So the picker has no privileged path into the
479
+ painter, and what it wrote is left in the editor to be read and changed.
480
+ Which element a preset lands on is decided by the plugin's LAYER, asked of
481
+ the painter rather than listed in the page: a source effect becomes the sky's
482
+ own background, a backdrop effect goes on the pane over it, because it draws
483
+ what is BEHIND an element and the opaque sky would paint over it a moment
484
+ later — so rain arrives as rain on the glass, with the stars bending through
485
+ it. The preset file reaches the bundle as the FILE, so a preset edited there
486
+ is the one the picker offers, the contact sheet paints and the pixel gate
487
+ checks; `page-check` reads it too and holds the round trip to it — every
488
+ preset offered, the file's own numbers in the display list, and the
489
+ stylesheet in the page saying what is on screen.
490
+
491
+ - **Three quieter surface effects, and a file of presets.** `plasma-wave`
492
+ (ribbons of light over a noise field), `raindrop` (drops on the pane, each a
493
+ sphere's lens over what is behind it — strongest at the rim, nothing in the
494
+ middle, so the page stays legible through the centre) and `ambient-light` (a
495
+ slow desaturated wash with bokeh discs in it). All three are plugins in the
496
+ same registry the starfield and the glass use: a name, a parameter list and
497
+ one GLSL function, with the box coming from the layout. Two of them are
498
+ BACKDROP effects, which is what makes a drop a lens rather than a sticker.
499
+ `lib/evg/gl/effect-presets.css` holds eleven ready blocks — five skies, two
500
+ plasma fields, two rains, two washes — paste-able into the editor under the
501
+ gallery's effects demo. `npm run evg:fx:shots` paints all of them into one
502
+ sheet, and `evg:fx:check` reads the same file through the engine's own
503
+ cascade and then against pixels, so a preset the cascade refuses fails a
504
+ check instead of quietly drawing the plugin's defaults.
505
+
506
+ - **The effects demo's stylesheet is editable in the page.** A textarea under
507
+ the canvas on `?demo=effects`, holding `effects.css`, applied as you type —
508
+ and what is typed goes through the WHOLE engine: `EffectsDemo.init` hands the
509
+ text to `EVGStyleSheet`, the cascade applies it, the layout lays the tree out
510
+ again, the display list carries whatever effect instances the sheet declared,
511
+ and the painter looks their names up. Nothing patches a parameter behind the
512
+ scenes, so `evg-fx-density: 6` on `.fx-sky` reaches the shader the same way
513
+ it does on a page nobody is editing. What the cascade refuses is reported in
514
+ the cascade's own words — type `#fx-sky` and it says the selector is
515
+ unsupported, because `EVGStyleSheet` keeps every declaration it rejected.
516
+ It is on this page and not on the standalone `lib/evg/gl/fx-demo.html`
517
+ because the bundle here carries the compiled engine and that page carries
518
+ only a display list built for it.
519
+
520
+ - **A switch per effect in the gallery rail**, beside the demo it belongs to.
521
+ The list is built from the DISPLAY LIST rather than from names written into
522
+ the page: it knows that the stylesheet declared four effects and what each is
523
+ called, and nothing more. Turning one off sets the flag the painter reads —
524
+ the pass is skipped, the shader never runs, a press on a sleeping card goes
525
+ nowhere, and nothing is rebuilt to stop drawing one shader. The ordinary CSS
526
+ stays: switch the glass off and the card is still a rounded, tinted,
527
+ `backdrop-filter`-blurred box, which is the clearest way to see where the
528
+ effect ends and the stylesheet begins. `page-check` drives the switch in a
529
+ real page and holds the painter to it — one pass fewer with the sky off, and
530
+ back again.
531
+
532
+ - **The surface effects are on the published gallery page** —
533
+ [`/ui/demo/?demo=effects`](https://terotests.github.io/Ranger/ui/demo/?demo=effects).
534
+ Every other demo there is a control measured against the component it copies;
535
+ this one is a MATERIAL, and what it shows is that the effects are declared in
536
+ `gallery/ui/demo/effects.css` and nowhere else. `EffectsDemo.rgr` holds no
537
+ shader, no clock, no parameter and no coordinate: it builds a tree, the sheet
538
+ says which box has which effect, the layout says where the box is, and the
539
+ painter looks the name up in its registry. The page's own share is four lines
540
+ — a press, a drag, a release and "is anything still moving" — handed to the
541
+ driver in `lib/evg/gl/evg-fx.js`, which now runs for any demo whose list
542
+ carries effects.
543
+
544
+ - **Liquid glass takes a sweep of light.** A bar crossing the pane at its own
545
+ angle, flaring where it meets the rim, either parked or travelling:
546
+ `evg-fx-sweep`, `-sweep-angle`, `-sweep-width`, `-sweep-speed`, `-sweep-at`,
547
+ `-sweep-edge`, `-sweep-rim` and `-sweep-duty`. The last two are what make it
548
+ a shine rather than a stripe: `sweep-rim` weights the bar toward the bevel,
549
+ where real glass catches a moving light, and `sweep-duty` gives the pass a
550
+ fraction of each cycle and parks it off the pane for the rest — so the pane
551
+ is clean glass most of the time and the glint is an event. Both are checked
552
+ against pixels: the flat middle comes back unchanged under a rim-weighted
553
+ bar, and between passes nothing on the pane is brighter than a pane with no
554
+ sweep at all. It is off by default, because a pane in a room with nothing
555
+ moving has no streak on it, and a moving one asks for frames with the same
556
+ `evg-effect-on: always` a starfield uses. The dashed names are the point:
557
+ `evg-fx-sweep-speed` reaches the shader as `p_sweep_speed` and nothing in
558
+ between had to learn either spelling.
559
+
560
+ - **A surface effect can be switched off, and the demo page has a switch for
561
+ each one.** `inst.off` on an instance: the painter skips the run, the filter
562
+ is not live, nothing is compiled or copied, and the driver stops handing it
563
+ events or counting it busy — so a press on a sleeping pool falls through to
564
+ what is under it. The frame is not rebuilt, which is the point: stopping one
565
+ shader must not mean re-uploading every buffer on the page. It is the hook a
566
+ page's own switch hangs on, and the one `prefers-reduced-motion` would;
567
+ `fx-demo.html` builds a switch per effect out of the display list and
568
+ remembers the choice per browser.
569
+
570
+ - **Liquid glass, as CSS.** A third plugin layer and the effect that needed it.
571
+ `backdrop` runs a plugin IN PAINT ORDER over what is behind the element — the
572
+ surface so far is copied where the element paints, the plugin writes it back
573
+ inside the box, and the element's own background, border and children are
574
+ drawn on top, sharp. That is what `backdrop-filter: blur()` has always done
575
+ for one hard-wired filter, now open to any plugin, and it is what a pane of
576
+ glass needs: one that ran as a post-pass would smear its own label.
577
+ `liquid-glass` is refraction rather than fog — the page behind dragged toward
578
+ the rim, compressed into a band a few pixels wide, split slightly into colour,
579
+ with a specular arc inset from the very edge so it reads as a bevel and not as
580
+ a border somebody drew. It knows no geometry of its own: the bend follows
581
+ `fxBoxDistance`, the rounded-box signed distance the preamble now hands every
582
+ plugin, so a pane is a lens at whatever size the layout gave it and whatever
583
+ `border-radius` the sheet asked for. Frosting, tint and shape stay ordinary
584
+ CSS beside it (`backdrop-filter`, `background-color`, `border-radius`), and
585
+ only the lens is the effect. Checked against stripes, where a displacement is
586
+ visible: the rim bends, the flat middle stands still, the page around it is
587
+ untouched and what is drawn over the pane stays exactly its own colour —
588
+ `npm run evg:fx:check`.
589
+
590
+ - **A surface effect belongs to an ELEMENT now, and is declared in CSS.**
591
+ `evg-surface-effect: ripple` was one effect over the whole page, with its
592
+ parameters as fields on `EVGElement` and its drops pushed in by the
593
+ application from its own `pointerdown` — every press, anywhere, rippled
594
+ everything, and a second effect could not exist because its parameters would
595
+ have had to be fields on every element in every document. Three properties
596
+ replace all of that: `evg-surface-effect` names what runs, `evg-effect-on`
597
+ (`press drag hover always`) says what starts it, and `evg-fx-<name>: <number>`
598
+ carries parameters the engine never reads. WHERE is the element's own border
599
+ box, because the layout already knows it — so an effect reflows with the box
600
+ it belongs to and no application holds a coordinate, a clock or a drop.
601
+ `EVGDisplayList` writes one instance per element under the element's `id`, and
602
+ the rectangle where that element paints carries `efx` so the effect has a
603
+ place in paint order. The original whole-surface `list.effect` is untouched
604
+ and still runs. `npm run evg:fx:test`.
605
+
606
+ - **Renderer plugins, and a starfield to prove it takes one.** The WebGL
607
+ painter now looks an effect's name up in a registry rather than branching on
608
+ it: `registerSurfaceEffect({ name, layer, params, frag })`, where `layer` is
609
+ `source` (drawn in paint order at the element's background, so the element's
610
+ content is painted over it) or `filter` (run over the finished surface,
611
+ clipped to the box, which is how the ripple bends text it knows nothing
612
+ about). A plugin writes one function, `vec4 fxColor(vec2 p, vec2 local)`, and
613
+ is handed the box, the radius, the clock and the pointer events; the rounded
614
+ box mask is applied in the shared `main`, so a plugin cannot paint outside
615
+ the element that declared it. The ripple became one of these without being
616
+ rewritten — `RIPPLE_CORE` is one string with two entry points. `starfield` is
617
+ the second: parallax star layers and two-hue nebula from hashes alone, no
618
+ state, no uploads, nothing in the display list but a name and nine numbers.
619
+ `lib/evg/gl/evg-fx.js` is the driver that turns a pointer into the events
620
+ they read, hit-testing the boxes the list carries. Checked in a real GL
621
+ context — scoped to its box, under the content, moving with the clock, and
622
+ leaking nothing outside — by `npm run evg:fx:check`; `npm run evg:fx:demo`
623
+ serves a page whose every effect comes out of a stylesheet.
624
+ [`lib/evg/PLAN_EFFECTS.md`](lib/evg/PLAN_EFFECTS.md).
625
+
626
+ - **The UI gallery is a github.io path.** `gallery/ui` already had the
627
+ tree-literal demos and the Radix-vs-Ranger playground; neither was in
628
+ the Pages artifact, so
629
+ [https://terotests.github.io/Ranger/ui/](https://terotests.github.io/Ranger/ui/)
630
+ 404'd. `deploy-pages.yml` now builds both (`/ui/demo/` and `/ui/web/`)
631
+ and `/ui/` redirects to the demos while keeping `?demo=`.
632
+
633
+ - **Two worked examples, as fixtures.** `lib/evg/agent/fixtures/popover.*` is a
634
+ menu bar whose open menu is anchored by name and becomes a bottom sheet
635
+ below 600px; `connector.*` is two cards with an arrow between them and a
636
+ count badge on a corner, in a grid that goes to one column under a media
637
+ query — the arrow follows without being mentioned. Both are a `.evg.json`
638
+ and a `.css`, rendered at two widths with
639
+ `npm run agent:render -- <doc> out.png -w <w> -h <h> -css <sheet>`.
640
+
641
+ - **Popovers: a surface knows where it fits, and what to be when it does
642
+ not.** EVG already drew overlay surfaces in a real top layer — after the
643
+ whole normal tree, outside every clip — and flipped them at a page edge.
644
+ Four things now turn that into a menu system. `position-anchor: --file`
645
+ names the box to position against (an `anchor-name` or an `#id`) instead of
646
+ finding it among the surface's own siblings, so a menu no longer has to be
647
+ declared beside its trigger; it also marks the element as a surface, as does
648
+ the new `popover` tag. `position-area: bottom start` is the side and the
649
+ cross-axis alignment in one declaration, and `position-try-fallbacks:
650
+ "top start, right start"` is an ordered list of areas tried until one is
651
+ wholly on the page — `position-try-order: most-space` takes the roomiest
652
+ instead of the first. `fit-viewport: true` clamps a surface to the room it
653
+ actually has and, with `overflow` set, `scrollHeight` is the rest of the
654
+ menu. `presentation: anchored | sheet | fullscreen` and `sheet-below: 600px`
655
+ cover the case no placement can: at 390 wide an anchored menu is the wrong
656
+ widget, so it becomes a sheet along the bottom edge with its children laid
657
+ out again at the page's width. `overflow-y`/`overflow-x` are accepted and
658
+ set `overflow`, which this engine has one of. And `anchor()` is read in the
659
+ inset properties — `left: calc(anchor(right) - 12px)` — which places one
660
+ edge against one edge of the anchor, with both insets on an axis stretching
661
+ the box between them; that is the badge-on-a-corner case no area can state.
662
+ [`lib/evg/EVGLayout.rgr`](lib/evg/EVGLayout.rgr),
663
+ `npm run evg:popover:test`, and the README's
664
+ [Surfaces](lib/evg/README.md#surfaces-popovers-anchors-and-presentation).
665
+
666
+ - **Connectors: a line between two elements, drawn by the layout.** A
667
+ `connector` names two boxes (`from`/`to`, an `anchor-name` such as
668
+ `--orders` or an `#id`), and EVG writes its `d` on every layout pass from
669
+ the rectangles they came out as. `from-side`/`to-side` default to `auto`,
670
+ which picks the facing pair — so the same connector leaves the right edge
671
+ while two cards sit side by side and the bottom edge once the grid stacks
672
+ them on a phone. `routing` is `straight`, `orthogonal` or `bezier`;
673
+ `arrow-start`/`arrow-end` are `open` (stroked) or `triangle` (filled) at
674
+ `arrow-size`; everything else is the stroke vocabulary a `path` already
675
+ has. `path` is unchanged and still the right tool when the author owns the
676
+ geometry. [`lib/evg/EVGConnector.rgr`](lib/evg/EVGConnector.rgr),
677
+ `npm run evg:connector:test`.
678
+
679
+ - **Erazer turns a UI screenshot into an EVG layout.** `gallery/erazer`
680
+ grows colour regions, nests them, and guesses widget classes (button,
681
+ text field, tab, menu, checkbox, slider, label, icon) instead of tracing the
682
+ picture as ink the way `EvgBitmapTracer` does. Icons that remain are
683
+ handed to that tracer as SVG. `npm run erazer:test` paints synthetic
684
+ UI-library fixtures with a 5×7 face and checks the tree; `npm run
685
+ erazer -- in.png out.evg.json` is the CLI; `npm run erazer:web:serve`
686
+ is the live page (paste or pick a screenshot in the tab; also at
687
+ https://terotests.github.io/Ranger/evg/erazer/ once Pages deploys). `npm run erazer:shots` captures HTML/CSS widgets
688
+ (login, tabs, menu, toolbar, dialog, buttons, sidebar) and a
689
+ shadcn/ui-shaped dark zinc dashboard, plus the live page itself.
690
+ The live overlay is pinned to the image (not the padded stage) with a
691
+ **tausta** opacity slider. Same-row chips of equal height and fill with
692
+ one-line labels are promoted to buttons together; a wide thin bar with
693
+ an optional circular thumb is a **slider**.
694
+ - **CodeGraph diffs two git revisions.** `codegraph_cli … --diff=base..head`
695
+ (or `--diff=base` against the working tree) and the desktop **Diff**
696
+ button check each side out as a detached worktree, analyse it the way
697
+ Open does, and compare classes by name and members by name: fields whose
698
+ type changed, methods whose signature changed, and methods whose lines
699
+ the file's line diff touched are `changed`; the rest `added` / `removed`.
700
+ The explorer opens on a diff page of only the touched classes (amber /
701
+ green / red), class pages keep the colours on their rows, and the source
702
+ pane shows touched files merged, removed lines in place on red bands.
703
+ The desktop rail lists the opened repository's log as base / head
704
+ pickers, and a pull request field (`12`, `#12`, a URL) fetches
705
+ `refs/pull/N/head` from origin and diffs from the merge base with the
706
+ target branch (`gh` when installed, else the remote's default branch);
707
+ the CLI takes `--pr=`. Rows keep their ink and carry a mark (`+`, `−`,
708
+ `Δ`) with a tooltip saying why; a `+ N more` row is marked when the
709
+ folded members include a change, and clicking it lists every member of
710
+ the class in a drawer that slides in from the left and leaves the chart
711
+ and the source pane usable — `gallery/ui` gained `DrawerCtl`, a
712
+ non-modal panel any host can fill, for it. The source pane copies its selection with Ctrl+C
713
+ (the tab through the clipboard API, the desktop through SDL) and, in
714
+ the tab, measures with the Noto Sans face it draws with rather than a
715
+ bitmap font's fixed step, which had spread the tokens of a line apart.
716
+ `CodeGraphDiff` needs neither git nor the
717
+ compiler; the web page's EXAMPLE menu diffs `calls.rgr` against
718
+ `calls_v2.rgr` in the tab.
719
+ RangerFlow rows gained `tint` / `tintText` and ScriptEditor `lineMarks`
720
+ for this. `npm run codegraph:diff` is the unit suite.
721
+
722
+ - **EVG live build** (`gallery/evg/livebuild/`). A demo of streaming an EVG
723
+ display list to a browser while an agent constructs the tree: thinking
724
+ tokens, `EVGPatch` ops, Ranger source and frames on SSE. The recipes are
725
+ scripted (dashboard, settings, invoices) so the pictures always land; a
726
+ live model that wrote the same ops would use the same socket.
727
+ The page is a local orchestrator (`agents.mjs`): recipe is the default,
728
+ Codex / Claude Code / Cursor / Ollama are adapters when those CLIs are
729
+ present. `npm run livebuild:serve` (no keys). `npm run livebuild:withcursor`
730
+ drives the local Cursor Agent CLI (`agent login` or `CURSOR_API_KEY`).
731
+ The prompt plus **Follow up** edits the live phone; Start-over chips
732
+ (Dashboard / Empty) are the only wipe. `npm run livebuild:test`.
733
+
734
+ ### Changed
735
+
736
+ - **Generated-code quality scores are recomputed on this compiler.**
737
+ The twelve `gallery/friendly` studies were compiled again after native
738
+ loops (#1024), C++ value records, behaviour-only traits as interfaces,
739
+ and the C++/Rust optional fixes. Rank is now Kotlin 76, Dart 73, C# 72,
740
+ Python 67, Swift 64, Rust 61, PHP 59, TypeScript 59, JavaScript 58,
741
+ Java 58, Scala 57, C++ 54, Go 48. PHP and Scala still emit index loops.
742
+
743
+ - **The front-page Targets section is generated-code quality status.**
744
+ The heading is "Generated code quality": an index of the thirteen
745
+ official source back ends, a score table, and the same two lists for
746
+ every language — what works well, and current limitations. The
747
+ percentage is the mean of twelve `gallery/friendly` study scores
748
+ (0–100, how close that file is to code a native developer would keep),
749
+ not a compile-success score. The "Write for the strictest target" and
750
+ "Extending it is not a rebuild" boxes come out. Swift 3, LLVM and WASM
751
+ stay out of the table.
752
+
753
+ - **The repository root, and the compiler folder, hold the product.** The root
754
+ had 71 markdown files, nine compiled JavaScript files, eleven buffer and JPEG
755
+ probes, a rustc log, a Node stack trace and a `pubspec.yaml` naming a Dart
756
+ package that lives under `gallery/`. It now has six documents and no build
757
+ output, and `.gitignore` covers every extension a compile run from the root
758
+ produces. Open plans moved to [`docs/plans/`](docs/plans/README.md), finished
759
+ ones and seven directories that are not the product — `adventofcode`,
760
+ `fiddle`, `rust_compiler`, `native`, `features`, `generated`, `versions` — to
761
+ [`legacy/`](legacy/README.md), each with a README saying what it was. The npm
762
+ scripts that drove `features/` and `generated/` went with them: they pointed
763
+ at `.rgr` files in folders that hold only `.clj`, so they had been failing.
764
+ README no longer offers `versions/<target>/compiler.js` as the rollback; the
765
+ git history of `dist/rgrc.js` is.
766
+
767
+ - **`compiler/` holds the compiler.** A walk of `Import` from the entry point
768
+ reaches 76 of the 134 `.rgr` files that were in the folder. The other 57 were
769
+ earlier editions kept beside the live file (`ng_RangerFlowParserOrig.rgr`,
770
+ `ng_FlowWork.rgr`, six `ng_parser*` variants), twenty probes from before
771
+ `tests/` existed, ten plugin samples that nothing loads by path — a compiler
772
+ plugin is an npm package reached with `require` — and fifteen `.bat` scripts
773
+ calling `ranger-compiler -compiler` on `.clj` files. `compiler/index.js`, a
774
+ 1.0 MB generated compiler, and `compiler/package.json`, declaring version
775
+ 2.1.61, went with them; what ships is `dist/rgrc.js`.
776
+
777
+ - **A compiler file is named after the class in it.** The `ng_` prefix came
778
+ from the compiler that preceded this one and none of the classes carry it.
779
+ 59 files renamed, every `Import`, npm script, test and document rewritten in
780
+ the same commit so no file is reachable by two spellings (ISSUES.md #64).
781
+ `ng_parser_v2.rgr` became `RangerLispParser.rgr`, `ng_parser_std_match2.rgr`
782
+ `FlowStdMatch.rgr`, `ng_writer.rgr` `CodeWriter.rgr`.
783
+
784
+ - **The three files over 9000 lines are split by what each part does.**
785
+ `RangerFlowParser` 9364 → 4702 (`FlowShape`, `FlowCollect`, `FlowTypes`,
786
+ `FlowTree`, `FlowImport`, `FlowProcess`), `RangerRustClassWriter` 12047 →
787
+ 3056 (`RustCall`, `RustOperators`, `RustClass`, `RustOwnership`,
788
+ `RustUnion`), `LowIRBuilder` 12302 → 3695 (`LowIRExpr`, `LowIRCollections`,
789
+ `LowIRStmt`, `LowIRObject`, `LowIRLambda`, `LowIRExtern`, `LowIROwnership`).
790
+ Each uses the `extension` form `FlowEnterVarDef.rgr` and `FlowStdMatch.rgr`
791
+ already used, so the class stays in one place and only methods move — every
792
+ body verbatim. Every compiler file is now under five thousand lines except
793
+ `Lang.rgr`, which is the `language { }` document, not a class file.
794
+ The emitted compiler is the same code in a different order, and compiling
795
+ again from it is a fixpoint.
796
+
797
+ - **The front-page hero columns are "There is no Silver Bullet." and
798
+ "Stay Dry."** Ranger is more like golden — a golden-file harness that
799
+ emits ordinary Swift, Kotlin and JavaScript, not a magic runtime. The
800
+ definition sits as a small footnote under the first column.
801
+
802
+ - **The gold Native section is "Native apps without duplicating the
803
+ logic."** One source of shared logic compiled to Swift and Kotlin,
804
+ called from SwiftUI and Compose with no bridge; a rule changes once
805
+ in the `.rgr` file. The Shopify quote and the six-up feature grid
806
+ come out.
807
+
808
+ - **The public site title is "Rewrite? Use Ranger."** The front page
809
+ heading is two lines — "Rewrite?" then gold "Use Ranger." — and the
810
+ document title, Open Graph title, documentation `<title>` suffix
811
+ (`Page | Rewrite? Use Ranger.`), and playground tab title all use that
812
+ line. The docs header still says Ranger.
813
+
814
+ - **Getting started step 03 is the edit loop, not the old exit-status bug.**
815
+ The front page's path is now: start from the starter, compile the targets,
816
+ change `src/Main.rgr` and run it again, then take a gallery package when
817
+ you need one. Generated output stays ordinary source you can open and
818
+ diff; the `.rgr` file is the source of truth.
819
+
820
+ - **EVG is MIT and lives in `lib/evg`.** The layout engine moved out of
821
+ `gallery/` (AGPL) to `lib/` (MIT) together with the image codecs it needs,
822
+ which are now the package `lib/image` (`Buffer`, `ImageBuffer`,
823
+ `RasterBuffer`, the JPEG decoders, `PNGDecoder`, `PNGEncoder`, `Deflate`;
824
+ moved from `gallery/pdf_writer/src` and `gallery/game_engine/lpc`).
825
+ `lib/zip` is the package `zip`. The parts of EVG that need the gallery's
826
+ rasteriser and font engine — `EVGWindow`, `EVGTextFit`,
827
+ `EVGContextMeasurer`, `EVGRulerView`, `EVGToolbarView` — are the AGPL
828
+ package `gallery/evg_window`. Every gallery project that draws through EVG
829
+ now has a `ranger.json` and imports it as `pkg:evg/…` (and `pkg:image/…`,
830
+ `pkg:evg_window/…`) instead of `../evg/…`, so `lib/evg` compiles the same
831
+ inside this tree and when fetched alone with `rgrc install`. See
832
+ LICENSING.md.
833
+
834
+ - **The JavaScript compiler compiles about twice as fast.** RtHost.rgr
835
+ (157 files, 108 102 lines) went from 17.9 s to 8.5 s, and the compiler
836
+ compiling itself from 19.9 s to 6.6 s, with byte-identical output apart
837
+ from the constructor lines below. Four causes, none of them in the
838
+ algorithms:
839
+ - A field with no initial value was left out of the constructor and
840
+ appeared on the object at first assignment, so one class had as many
841
+ V8 hidden classes as there were assignment orders (CodeNode: 27). Every
842
+ property access on such objects went megamorphic and the optimiser
843
+ deoptimised in a loop. The ES6 writer now initialises every field, to
844
+ `undefined` where there is no value; `typeof`, `== null` and
845
+ `JSON.stringify` read the same as before.
846
+ - V8 gives a field the representation of the first value stored in it.
847
+ `CodeNode.double_value` and `int_value` began as small integers, and
848
+ the first fractional literal in a program changed the representation
849
+ on every node already made, each of which was then rewritten on its
850
+ next access -- 2.4 s of the RtHost compile, paid by whichever pass
851
+ first walked the tree. The parser now stores such values once, when one
852
+ node exists.
853
+ - `read_file` awaits `fs.readFile`, so every function that reads a file,
854
+ and every function that can reach one, was emitted `async`: in the
855
+ compiler that was the whole import, analysis and writer chain, and each
856
+ call paid for a promise and a heap frame -- 6.3 GB allocated and three
857
+ seconds of garbage collection per compile. New `read_file_sync`
858
+ operator, synchronous on JavaScript, used by the compiler's own file
859
+ reads; the compiler now has no async method.
860
+ - Flags and classes live on the root context, but `hasCompilerFlag` and
861
+ `isDefinedClass` walked up the context chain to find them, nine million
862
+ and seven million times per compile. The root is now cached at fork.
863
+ `getOpFns`, asked for every call expression and empty nearly every
864
+ time, no longer builds and copies a list per level of the chain, and
865
+ `TTypeRegistry.isScalarPrimitive` no longer rebuilds its name list
866
+ for every element it compares against.
867
+
868
+ ### Fixed
869
+
870
+ - **A demo that moves on its own now gets a frame without being touched.** Every
871
+ path that started the gallery page's clock was an INPUT — a press, a key, a
872
+ focus — because every demo that moved did so in answer to one. A surface
873
+ effect does not, so `?demo=effects` opened on a still picture of a drifting
874
+ sky until you poked it. The clock is started on the first paint and when the
875
+ switcher changes demo; it stops on the first frame whose clock says nothing
876
+ is moving, which is all of them until something is.
877
+
878
+ - **A liquid-glass pane wiped everything painted before it, on any real page.**
879
+ A backdrop effect copies the surface mid-frame, and it was copying from the
880
+ canvas — but a WebGL2 context asked for `antialias: true`, which is the
881
+ default and what every page here asks for, has a MULTISAMPLED default
882
+ framebuffer, and copying out of one is `INVALID_OPERATION`. The copy left the
883
+ texture black and the pass wrote that black back over the whole page above
884
+ the pane. A frame with a live backdrop now goes through the offscreen target
885
+ and is presented at the end. The checks had passed on the broken painter
886
+ because they rendered with `preserveDrawingBuffer: true`, which on this
887
+ driver hands out a single-sampled buffer: `fx-check.mjs` now asks for what a
888
+ page asks for and reads pixels back through a 2-D canvas, and fails three
889
+ ways without the fix.
890
+
891
+ - **The UI demo page did not work on a phone.** `gallery/ui/demo` wrote each
892
+ demo's own width straight onto the canvas and let the rest hang off the
893
+ right-hand edge, where a finger could not reach it — and the 230px rail took
894
+ most of a 390px screen before the stage got any. The stage is now scaled to
895
+ the room the viewport has (`transform` on `#stage`, the wrapper carrying the
896
+ laid-out size, the backing store sized for the pixels actually on screen),
897
+ the canvas is the demo's OWN width rather than 1240 for all of them, and
898
+ under 860px the rail becomes a one-line `controls` disclosure above the
899
+ stage. `window.__stageScale` publishes the factor, because anything driving
900
+ the page from outside aims at display-list coordinates.
901
+
902
+ - **A canvas demo could not be reached from the keyboard at all.** The page
903
+ handed `state.focus` — its own field, kept for the menubar and nothing else
904
+ — to every demo's `a11yJson`, so the accessibility mirror was told nothing
905
+ was focused on nineteen of the twenty. With a roving tabindex that means NO
906
+ element is a tab stop: Tab walked straight past the dropdown, the tree and
907
+ the table. Each demo's own focus now reaches the mirror, focus arriving by
908
+ Tab is reported back to the demo (`onFocus`), and `evg-a11y.js` keeps one
909
+ entry tab stop while an app names no focus — a roving pattern always has
910
+ one. Tab is also no longer swallowed: `DropdownDemo.key` answers "taken" to
911
+ any key at all, so the page called `preventDefault()` on Tab and the focus
912
+ could neither enter nor leave.
913
+
914
+ - **The keyboard moved the selection and the highlight stayed behind.** Every
915
+ pointer handler on the demo page started the animation clock; the keydown
916
+ handler painted once and stopped. A row's background is a transitioned
917
+ property, so the one frame a key produced was the frame the transition had
918
+ not started in — the tree's grey sat on the row the arrow had just left, and
919
+ moved only when something else repainted. Arrowing the tree, the dropdown
920
+ and the table now looks like what it does.
921
+
922
+ - **The table demo had no keyboard.** `main.js` gave it `key: () => false` and
923
+ `TableDemo` had nothing to hand a key to. It now carries a roving focus over
924
+ the ring its own tree publishes as focusable — the select-all box (which was
925
+ not focusable and now is), the sortable headers, the row boxes and the pager
926
+ — with the arrows to walk it, Home and End, and Enter or Space to work the
927
+ control you are on. `table.css` grew the `:focus` rules without which none
928
+ of that was visible.
929
+
930
+ - **The invoice form's Email validator never changed its mind.** The error was
931
+ a string assigned once in `FormDemo.init`, so the red ring and "That address
932
+ is missing an @." stayed on the field whatever was typed into it. It is
933
+ decided from the value on every rebuild now, and each message says what is
934
+ wrong — a missing @, a space, nothing before or after it, a host with no dot
935
+ — rather than that something is.
936
+
937
+ - **"Find a customer" searched nothing.** It was an `InputCtl` with a
938
+ magnifier drawn in front of it: a field that looks like an autocomplete and
939
+ filters no list. It is a `ComboboxCtl` now — the same one the metadata card
940
+ uses, measured against @base-ui/react/combobox — so typing filters, the list
941
+ opens under the box, the arrows walk it and Enter takes a row into the
942
+ field. `ComboboxCtl.applyEdit` is the new seam a host that lets the platform
943
+ do the editing needs: a keystroke and a browser edit have the same effect on
944
+ the list, and neither host has to reimplement it. The form claims the arrows
945
+ back from the text bridge for that field alone (`ownsKey`), because there
946
+ they walk the list rather than the caret.
947
+
948
+ - **`evg:view:check` flaked in CI on a landing-only pull request.** The
949
+ suite draws each scene twice — once with the view baked into the list,
950
+ once with it on the camera — by rewriting one `file://` HTML file and
951
+ navigating to it. Chromium will sometimes answer the previous document
952
+ for that URL, `waitForFunction` sees the leftover `__DONE__`, and the
953
+ comparison reads the last framebuffer: one of thirty-one checks fails,
954
+ the rest pass. A unique query on each load is a different navigation;
955
+ `gl.finish()` lands before the read; a mismatch draws both sides again
956
+ before it is a failure. The gallery-editors runner also printed only
957
+ the last thirty lines of a failed suite, which hid the `FAIL` behind
958
+ later PASSes.
959
+
960
+ - **The published compiler shipped a thinner `stdlib.rgr` than the one the
961
+ tests ran against.** `compiler/stdlib.rgr` and `lib/stdlib.rgr` had drifted:
962
+ the compiler's copy carried the LLVM `case` and `is` templates that let
963
+ `lowerShapeCase` bind its operand, and the Swift `_ = name` lines that stop
964
+ swiftc warning once per arm of an exhaustive `match`. Which copy you got
965
+ depended on where you compiled from. In the repository RANGER_LIB puts
966
+ `compiler/` first, so CI always exercised the fuller file; `build:dist:copy`
967
+ is `cp -r ./lib ./dist/lib` and the published package resolves every import
968
+ from there, so an npm user compiling a `match` to Swift got the warnings and
969
+ to LLVM got an unmatched operator. `lib/` now holds the merged file and the
970
+ duplicate under `compiler/` is gone. `compiler/JSON.rgr` was byte-identical
971
+ to `lib/JSON.rgr` and went the same way. Both imports keep their spelling;
972
+ resolution finds them one directory later.
973
+
974
+ - **`.claude/skills/ranger-lang/SKILL.md` said the compiler exits 0 on
975
+ `[FAIL]`.** It has exited non-zero since 3.5.1, and the copy under
976
+ `plugins/ranger/` already said so. The two copies had drifted in both
977
+ directions — the plugin copy still said `(obj.method()).field` does not
978
+ work — and are now identical and correct.
979
+
980
+ - **`@media` was silently inert in every CLI tool.** `EVGStyleSheet` evaluates
981
+ a media query against a viewport the caller states, and a query it cannot
982
+ evaluate does not apply — but `EVGStyleLoader`, which every `-css` flag goes
983
+ through, never stated one. So the responsive half of a stylesheet did
984
+ nothing in the PNG, PDF, HTML, JSON and display-list tools while the same
985
+ sheet worked in an application, with no warning either way: the page
986
+ rendered, and it rendered wrong. The tools know their page size before they
987
+ apply anything, and now pass it.
988
+
989
+ - **`position: absolute` was dropped inside a `display: grid` parent.**
990
+ `layoutGrid` left out-of-flow children out of the placement, which is
991
+ right — an absolute box takes no track — and then nothing laid them out at
992
+ all: the box kept zero size at (0,0), so an absolutely positioned `div`
993
+ vanished and a `path` drew its own coordinates in the page's top-left
994
+ corner with `left`/`top` ignored. The same element under a flex or block
995
+ parent was placed correctly, which made it look like a `path` bug. The
996
+ out-of-flow pass is now one function (`EVGLayout.layoutOutOfFlowChild`)
997
+ that both flow and grid run.
998
+
999
+ - **An array literal survives a call whose result is dereferenced.**
1000
+ `(box.take(([] _:string ( "a" "b" )))).count()` emitted
1001
+ `box.take("a""b").count()` — the elements where the array should be. Two
1002
+ elements did not parse on JavaScript or PHP; one element parsed and answered
1003
+ the STRING's length, so `take(([] _:string ( "abc" )))` counted 3. Every
1004
+ target, and the compiler reported success either way, which is why
1005
+ `lib/Shell.rgr` and `lib/apple/` build every argument vector with `push`
1006
+ rather than writing it inline.
1007
+
1008
+ `transformDotMethodCallExpr` walked the receiver — that is how it learns the
1009
+ receiver's type and decides this is a method call at all — and then put a
1010
+ COPY of the walked node into the rewritten `call`. Walking an array literal
1011
+ is destructive: the node's children become its elements and the node is
1012
+ marked `is_array_literal`, and the two together are what an array literal is
1013
+ after analysis. A copy is built for re-analysis and carries no analysis
1014
+ result, so it had the elements and no mark: a bare list. The receiver is
1015
+ copied before it is walked now, and that untouched copy is what the rewrite
1016
+ gets. ISSUES.md #85.
1017
+
1018
+ The one-element literal still goes missing on Rust, chained or not — that is
1019
+ #84, in the Rust writer.
1020
+
1021
+ - **A failed compile exits non-zero.** `rgrc` printed `[FAIL]` and
1022
+ `Compilation FAILED` and then returned success, so
1023
+ `rgrc x.rgr && node bin/x.js` was satisfied by that zero, ran the
1024
+ *previous* build, and printed what it printed before the change — the edit
1025
+ looked applied and the test looked green when neither was true. Every
1026
+ `&&` in every build script believed the zero, which is why a dozen scripts
1027
+ in this repository read the compiler's log instead of its status. The
1028
+ status now says what the report says: 1 when the run had errors (a parse
1029
+ error, a missing file, any compiler error, a failed `rgrc install`),
1030
+ 0 otherwise.
1031
+
1032
+ The code is set rather than the process ended on the spot: node writes a
1033
+ piped stdout asynchronously, and `process.exit()` there drops the error
1034
+ report the status is about — `log=$(rgrc … 2>&1)` would get the status and
1035
+ lose the reason. The new `set_exit_code` operator in `Lang.rgr` spells
1036
+ that on each target (`process.exitCode` on JavaScript,
1037
+ `Environment.ExitCode` on C#, the same immediate exit as `exit` where
1038
+ stdout is unbuffered or flushed at exit).
1039
+
1040
+ - **CodeGraph web page: css / evg / zip / cpp did not open from the EXAMPLE
1041
+ menu.** The app could only compile what its VFS held, and the gallery
1042
+ pack (and the C++ fixture) were fetched only for `?example=`. A pick the
1043
+ app cannot serve is now handed back to the page (`consumePendingSample`),
1044
+ which fetches the sources and picks again. The page also awaits the
1045
+ app's methods — the ES6 bundle makes any path that may read a file
1046
+ `async`, and `selfTest()` came back as a Promise, which is why
1047
+ `codegraph:web:test` failed with `nav.startsWith is not a function`.
1048
+ - **CodeGraph: a class did not list who uses it through a method.** Rose
1049
+ boxes above a class came from field types only, so `Checkout` showed no
1050
+ `CodeGraphFixtureMain` although `main()` creates one. Classes whose
1051
+ methods mention or call the class are now backrefs too, with the method
1052
+ rows and a dashed `uses` edge from each row.
1053
+
10
1054
  ## [3.5.1] - 2026-09-17
11
1055
 
12
1056
  - **The npm README still said 3.3.0.** The package was 3.5.0; the first line
@@ -22,7 +1066,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
1066
 
23
1067
  - **`rgrc install` fetches what `ranger.json` names.** `Import "pkg:…"`
24
1068
  used to resolve only against files already on disk, so a clean checkout
25
- of a project that depends on `gallery/evg` had nothing to compile
1069
+ of a project that depends on `lib/evg` had nothing to compile
26
1070
  against. The compiler now walks the graph, sparse-fetches each git
27
1071
  dependency at its pinned revision, writes it into the cache
28
1072
  (`RANGER_PKG_CACHE`, else `~/.cache/ranger/packages/<sha256>`), recurses
@@ -139,7 +1183,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
139
1183
  cell's text lands in the third cell.
140
1184
 
141
1185
  - **A design for the camera on the GPU.**
142
- `gallery/evg/PLAN_VIEW_TRANSFORM.md` asks why a canvas rebuilds its whole
1186
+ `lib/evg/PLAN_VIEW_TRANSFORM.md` asks why a canvas rebuilds its whole
143
1187
  display list for every frame of a pan, when the pan is a translate the vertex
144
1188
  shader already applies for scroll layers (`uShift`) and the scene has not
145
1189
  changed. The list would be built in scene space and carry the camera beside
@@ -156,7 +1200,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
156
1200
  - **Two fingers pinch the canvas, and the gestures are one module.** Drag to
157
1201
  pan, wheel to zoom, a press that does not travel is a click — every
158
1202
  standalone had written its own, and none of them had a pinch.
159
- `gallery/evg/gl/evg-gestures.js` is that handling once, for any EVG canvas:
1203
+ `lib/evg/gl/evg-gestures.js` is that handling once, for any EVG canvas:
160
1204
  it reads the view the host keeps and hands back another, so the host goes on
161
1205
  deciding when to paint. The anchor holds the point under the cursor, or
162
1206
  under the midpoint of two fingers, where it is; a trackpad pinch (a wheel
@@ -251,7 +1295,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
251
1295
  the page `EVGDisplayList.toBinary()` — three `Int32Array`s and a string
252
1296
  pool — where it used to hand it JSON: on a board of 3,565 nodes that is
253
1297
  5,630 ms a frame against 180, and the two bridges describe the same picture
254
- to the hundredth (`gallery/evg/gl/list-binary-check.mjs` holds them to it).
1298
+ to the hundredth (`lib/evg/gl/list-binary-check.mjs` holds them to it).
255
1299
  `scene()` still answers in JSON for anything that wants to read a frame.
256
1300
 
257
1301
  - **A flattened outline is kept on the element it belongs to.** `d` is a
@@ -824,7 +1868,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
824
1868
  for node. 101 + 102 + 17 + 16 assertions and 24/24 target builds; the client
825
1869
  build an app carries is 113 kB of Kotlin, which is the measured answer to
826
1870
  whether it fits on a watch. Plan and the phases left in
827
- [`PLAN_FIRESIM.md`](PLAN_FIRESIM.md).
1871
+ [`PLAN_FIRESIM.md`](gallery/firesim/PLAN_FIRESIM.md).
828
1872
  - **A segmented date field, measured against the browser's own.** The
829
1873
  calendar demo's date box was a formatted label; a person asked for the
830
1874
  `__/__/____` editor, and shadcn has none to measure (its Date Picker is a
@@ -1046,7 +2090,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
1046
2090
  `ranger/rt_ios.rgr` imports `RealTrainerDemo.rgr` unchanged and compiles to
1047
2091
  19 000 lines of Swift holding the EVG controllers, the cascade, the layout
1048
2092
  engine, the display list and the demo. Nothing about the app is written twice
1049
- for Apple; `gallery/evg/apple` paints it, as it paints the dashboard.
2093
+ for Apple; `lib/evg/apple` paints it, as it paints the dashboard.
1050
2094
 
1051
2095
  The facade is not a copy of the dashboard's, because the two pages are not
1052
2096
  the same shape. The dashboard is a DOCUMENT -- a fixed width that scrolls, so
@@ -1077,7 +2121,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
1077
2121
 
1078
2122
  ### Fixed
1079
2123
 
1080
- - **`gallery/evg`: a grid flag nothing read.** `EVGLayout.layoutGrid` set
2124
+ - **`lib/evg`: a grid flag nothing read.** `EVGLayout.layoutGrid` set
1081
2125
  `usingSubgrid = true` when a column template inherited its tracks from the
1082
2126
  enclosing grid, and then never looked at it. The subgrid effect is carried
1083
2127
  entirely by rewriting `colSpec` into the parent's pixel tracks, and the rows
@@ -1361,7 +2405,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
1361
2405
  iPhone, iPad and Apple Watch.** The same `DashboardDemo.rgr` the browser
1362
2406
  page, the gates and the Android port run, compiled to Swift 6 (46 039 lines,
1363
2407
  one file) and painted with CoreGraphics through the new
1364
- [`gallery/evg/apple`](gallery/evg/apple/README.md) — an `EvgSurface` protocol,
2408
+ [`lib/evg/apple`](lib/evg/apple/README.md) — an `EvgSurface` protocol,
1365
2409
  an `EvgPainter` transliterated from the Android one, and a single `CGContext`
1366
2410
  backend that serves UIKit, SwiftUI and watchOS alike.
1367
2411
 
@@ -1394,7 +2438,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
1394
2438
  source: Kotlin on a JVM (the Wear OS language, with a C1-only run as the
1395
2439
  ART-quality bracket), C++ ahead-of-time and reference-counted (the watchOS
1396
2440
  proxy), and JavaScript on Node. The Kotlin run paints through
1397
- `gallery/evg/android`, the same painter and surface the `ui` and `pptx`
2441
+ `lib/evg/android`, the same painter and surface the `ui` and `pptx`
1398
2442
  Android ports compile into their APKs, and writes the three PNGs.
1399
2443
 
1400
2444
  The answer is **no, EVG is not too heavy**: the busiest screen is 0.91 ms for
@@ -2290,7 +3334,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2290
3334
 
2291
3335
  - **Three defects in Vela that only a program calling it could reach** — each found by building six charts through the new chart API and comparing the drawing against official Vega-Lite and Vega, and none of them in the API. `%q` was **not a directive** in the time formatter, and an unknown directive answers its own letter, so every chart with `timeUnit: "yearquarter"` — which the official compiler turns into the format `"%Y Q%q"` — labelled its axis `2016 Qq` and said nothing about being wrong. A **grouped bar chart of stated width drew twenty-pixel bars**: which way round the two bands are sized depends on who decided the plot's size, and the sub-scale was always given a fixed twenty-pixel step, which is right only when nobody said how wide the plot is (then the band above it is sized *from* the sub-bands) and leaves a third of every group empty when somebody did — the sub-scale has to span the band it sits in, `[0, bandwidth('x')]`, exactly as the reference writes it. And **the key to a see-through area was drawn solid**, because a legend symbol's opacity was read only off an `opacity` channel and not off the mark (`{"type": "area", "opacity": 0.35}`), which is how every layered chart shades its filled half. All 48 goldens, 4848 drawn primitives, 44 Vega-Lite sources and the 188-example gallery corpus (172 exact, 3 random-by-construction) are unchanged by the three
2292
3336
 
2293
- - **A long value in a dialog field was drawn straight out of the field, across the panel and over the sheet behind it** — `EVGWindow`'s input control drew `c.value` from the field's left edge at full length, with no window and no cut. Clipping alone does not fix it: `SoftPainter` honours a clip rectangle **per draw call** (`textInClip` tests the text's ORIGIN), so a string that starts inside the box is drawn whole, and the WebGL and SDL hosts scissor at their own granularities again. Text that must not escape a box has to be cut to the box on every backend. The cut now lives in one place, `gallery/evg/EVGTextFit.rgr`: the window that fits the field **and** contains the caret (grown left from the caret while there is room, then right with what is left, so typing at the end scrolls the text and moving back brings the earlier text into view), the measured caret offset inside that window, a hit test measured the same way, and the label trimmer the grid already had. `EVGWindow`'s input, the grid's cell labels and the formula field all go through it. The display-list primitives moved to `EVGDisplayList` itself (`addRect` / `addFrame` / `addText` / `addClip` / `addClipEnd`) — `GridView` and `EVGWindow` each had their own encoder for the same commands — so a dialog and the grid behind it cannot drift into two encodings. `npm run datagrid:textfit:test` checks the display list rather than the picture: for every piece of text that starts inside a panel, does it end inside it too? It fails on the code from before the fix. Also: the database window smoke now addresses cells by name (`nav.goto E2`) instead of by pixel, after a merge with master moved the grid geometry under its hard-coded clicks
3337
+ - **A long value in a dialog field was drawn straight out of the field, across the panel and over the sheet behind it** — `EVGWindow`'s input control drew `c.value` from the field's left edge at full length, with no window and no cut. Clipping alone does not fix it: `SoftPainter` honours a clip rectangle **per draw call** (`textInClip` tests the text's ORIGIN), so a string that starts inside the box is drawn whole, and the WebGL and SDL hosts scissor at their own granularities again. Text that must not escape a box has to be cut to the box on every backend. The cut now lives in one place, `gallery/evg_window/EVGTextFit.rgr`: the window that fits the field **and** contains the caret (grown left from the caret while there is room, then right with what is left, so typing at the end scrolls the text and moving back brings the earlier text into view), the measured caret offset inside that window, a hit test measured the same way, and the label trimmer the grid already had. `EVGWindow`'s input, the grid's cell labels and the formula field all go through it. The display-list primitives moved to `EVGDisplayList` itself (`addRect` / `addFrame` / `addText` / `addClip` / `addClipEnd`) — `GridView` and `EVGWindow` each had their own encoder for the same commands — so a dialog and the grid behind it cannot drift into two encodings. `npm run datagrid:textfit:test` checks the display list rather than the picture: for every piece of text that starts inside a panel, does it end inside it too? It fails on the code from before the fix. Also: the database window smoke now addresses cells by name (`nav.goto E2`) instead of by pixel, after a merge with master moved the grid geometry under its hard-coded clicks
2294
3338
 
2295
3339
  - **A declared empty constructor produced a body-less `__init__` on the Python target** — `ng_RangerPythonClassWriter` already emits `pass` for an empty body, but it set `hasContent = true` for any class that *declared* a constructor rather than for one whose body actually wrote something. A class with no fields, no parent and `Constructor () { }` therefore compiled to `def __init__(self):` with nothing under it, and Python failed to parse the whole module at the next method (`IndentationError: expected an indented block`). Every other target was unaffected, because only Python's block structure is whitespace. It is now measured the same way `pyWalkBody` already measures a method body — the writer's line and column before and after walking the constructor — and a `super().__init__()` call counts as content, so a subclass with an empty constructor no longer gets both the super call and a stray `pass`. Found by RangerSQL's dialect classes, which are pure behaviour and declare no fields at all. Compiler reproduces itself byte-identically; `compiler-python`, `compiler-conformance`, `compiler-selfhost`, `compiler-record`, `compiler-serialize` and `operator-coverage` pass, and `shapes`' 14 Rust failures reproduce on the unmodified compiler
2296
3340
 
@@ -2309,7 +3353,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2309
3353
 
2310
3354
  - **The rules a photo book has to satisfy belong to the printer, so they are data now** — `gallery/book` could lay a book out and draw it; what it could not do was answer "is this ready to send". `BookPrintSpec` is a supplier's requirements as a value — trim size, bleed, outer **and gutter** safety margins, the extent's minimum, maximum and multiple, the dpi floor and target, colour space, PDF profile, whether they want crop marks — with presets for layflat 210, hardcover A4, softcover A5 and sheet-fed offset, and preflight measures the book against whichever one you name. Hard-coding any of it would have produced an engine that is confidently wrong for every supplier but one: 3 mm of bleed is a European convention and not a law, a multiple of four is one binding's arithmetic, and one service wants CMYK while the next one wants RGB. Three things stayed assertions rather than settings because they are not negotiable. **Page 1 is a recto** — odd pages right, even left, and a cover is always the right-hand side of the sheet it is bound onto. **Single pages, in reader's order**, 1, 2, 3, 4: printer spreads (32–1 / 2–31) are the press's business, and imposing them yourself is how a book comes back bound inside out — the engine designs in spreads and exports leaves, which is the whole point of a page-layout program. And **a blank page is a page**: it takes a leaf, it is counted, and it has to be *in* the file, so `padToExtent` adds real ones rather than leaving a gap that silently moves every page after it onto the wrong side of the leaf. The gutter check is the one that earns its keep: it is a **bigger** margin than the cut edges and it applies to whichever side of the page the spine is on, which is the left of a recto and the right of a verso — a face centred in a spread lands in the fold, and on a cased book the fold takes 15 mm. **The spine is arithmetic and `BookCover` does it**: leaves × caliper for the text block, plus two boards for a case or the wrap for a softcover, with the squares, the hinge and the turn-in giving the cover sheet its real size — built as its own one-page landscape document, so the same renderer, the same PDF writer and the same picture-resolution checks apply to it. It still says, in the output and in the README, to take the supplier's own template before a production run; this exists so a cover can be proofed before that template arrives and so their number can be checked against one. `npm run book:print` writes `interior.pdf` (single pages at trim + bleed, with a **TrimBox** — 210 mm square is delivered as 216 mm square), `cover.pdf`, a `print.json` manifest carrying the fields a print-on-demand API asks for, `preflight.txt`, and `render.sh` — the exact commands with the computed page sizes already in them, because a cover PDF made at a retyped width has the title on the front board and the spine somewhere in the picture. It found a live trap on the way: **the committed `evg_pdf_tool.js` build predates its own `-bleed` flag** and contains no reference to bleed at all, so running it with `-bleed` accepted the argument, warned about nothing, and wrote a PDF at trim size — invisible until a press trims into the artwork. The script recompiles the tool before using it. 40 more assertions, **116 on the engine now, running on JavaScript, Go and Python**, plus the 64 editor ones and 17 in a real browser
2311
3355
 
2312
- - **The book engine got an editor, and it runs in the page** — `gallery/book` could lay a book out and print it; it could not be *used*. Now it can: select a frame, drag it, resize it from eight handles, insert text and picture frames, reorder pages, undo. The frame around the canvas is the **shared** one the spreadsheet, the document and the deck already use — a fourth toolbar in a fourth style would have been the wrong kind of new code — and the selection chrome goes into the same `EVGDisplayList` as the pages, after them, so every host draws a complete editor without knowing what a handle is, and an exported book has none of it. Four rules are taken from the slide editor because they were right there and are right here (an id is not an index; history is snapshots; a drag is one edit; editing is a mode) and one is this program's own and is the whole difference: **every geometry edit re-flows** — resizing a text frame does not move text inside it, it moves text onto other pages, and an editor that does not re-flow after a drag is showing a book that does not exist. The gesture with no equivalent in a slide editor is **Link flow** (Ctrl+L): select two text frames and the story runs from one into the other. It moves no text; it changes where the text is *allowed* to go. **It is not a Node app with a browser attached.** `web/standalone/` compiles the entire engine — model, flow, preflight — to JavaScript and puts it IN the page: the browser hands it font bytes and photographs and draws the display list it gets back through WebGL 2, so a pointer move is a function call rather than a round trip and there is no host process at all (`npm run book:web`). The Node-hosted variant stays as `book:window`, for driving the editor from a script. Three findings came out of building it, all of the kind a screenshot makes look fine. **Snapping per-delta means a frame on a guide can never be dragged off it** — every one-point step is pulled back onto the line — so a drag is now measured from where it *began*. **A host that hands over every font face with `addFaceBytes` gets a renderer that draws correctly and measures with a 3×5 bitmap fallback**, because only `loadFontBytes` sets `hasFont`; the page was drawing the title in exactly the right typeface, at the wrong widths, so nothing ever wrapped. And **a WebGL page needs `@font-face` for the faces the engine measured with**, since `evg-webgl.js` rasterizes runs with the browser's canvas 2D and cannot see the engine's. The serverless page's self test now asserts the title wraps *and* that it is set in the display face, which is the pair that catches all three. Four things moved into `gallery/evg` because the book editor was the second caller: `EVGImageDecode` (PNG/JPEG bytes → pixels — it had been sitting in the deck viewer with a note on it saying it was not deck-specific), `EVGSelectChrome` (where the eight handles are and which edges each one owns — both editors now number their handles from the same file and read `applyResize`'s answers out of it), `EVGContextMeasurer` (EVG text measurement backed by a host's own renderer, which is what makes the flow engine measure with the faces the screen paints with), and `EVGDisplayList.offsetBy` / `.appendFrom`. The sample book is typeset rather than filled in — the showcase's editorial palette, Cinzel and Josefin Sans, a leaf ornament as a vector frame, captions on the pictures, first-line indents after the first paragraph — because a sample set entirely in the UI face proves nothing about a page-layout engine. **80 assertions on the engine (76) run on JavaScript, Go and Python; 64 more cover the editor and the host seam; 17 run in real headless Chrome on WebGL**, and `pptx`, `docx_viewer`, `datagrid` and the EVG toolbar suites are unchanged by the extractions (33 / 220 / 100 / 125 / 25, 244, 113, 23)
3356
+ - **The book engine got an editor, and it runs in the page** — `gallery/book` could lay a book out and print it; it could not be *used*. Now it can: select a frame, drag it, resize it from eight handles, insert text and picture frames, reorder pages, undo. The frame around the canvas is the **shared** one the spreadsheet, the document and the deck already use — a fourth toolbar in a fourth style would have been the wrong kind of new code — and the selection chrome goes into the same `EVGDisplayList` as the pages, after them, so every host draws a complete editor without knowing what a handle is, and an exported book has none of it. Four rules are taken from the slide editor because they were right there and are right here (an id is not an index; history is snapshots; a drag is one edit; editing is a mode) and one is this program's own and is the whole difference: **every geometry edit re-flows** — resizing a text frame does not move text inside it, it moves text onto other pages, and an editor that does not re-flow after a drag is showing a book that does not exist. The gesture with no equivalent in a slide editor is **Link flow** (Ctrl+L): select two text frames and the story runs from one into the other. It moves no text; it changes where the text is *allowed* to go. **It is not a Node app with a browser attached.** `web/standalone/` compiles the entire engine — model, flow, preflight — to JavaScript and puts it IN the page: the browser hands it font bytes and photographs and draws the display list it gets back through WebGL 2, so a pointer move is a function call rather than a round trip and there is no host process at all (`npm run book:web`). The Node-hosted variant stays as `book:window`, for driving the editor from a script. Three findings came out of building it, all of the kind a screenshot makes look fine. **Snapping per-delta means a frame on a guide can never be dragged off it** — every one-point step is pulled back onto the line — so a drag is now measured from where it *began*. **A host that hands over every font face with `addFaceBytes` gets a renderer that draws correctly and measures with a 3×5 bitmap fallback**, because only `loadFontBytes` sets `hasFont`; the page was drawing the title in exactly the right typeface, at the wrong widths, so nothing ever wrapped. And **a WebGL page needs `@font-face` for the faces the engine measured with**, since `evg-webgl.js` rasterizes runs with the browser's canvas 2D and cannot see the engine's. The serverless page's self test now asserts the title wraps *and* that it is set in the display face, which is the pair that catches all three. Four things moved into `lib/evg` because the book editor was the second caller: `EVGImageDecode` (PNG/JPEG bytes → pixels — it had been sitting in the deck viewer with a note on it saying it was not deck-specific), `EVGSelectChrome` (where the eight handles are and which edges each one owns — both editors now number their handles from the same file and read `applyResize`'s answers out of it), `EVGContextMeasurer` (EVG text measurement backed by a host's own renderer, which is what makes the flow engine measure with the faces the screen paints with), and `EVGDisplayList.offsetBy` / `.appendFrom`. The sample book is typeset rather than filled in — the showcase's editorial palette, Cinzel and Josefin Sans, a leaf ornament as a vector frame, captions on the pictures, first-line indents after the first paragraph — because a sample set entirely in the UI face proves nothing about a page-layout engine. **80 assertions on the engine (76) run on JavaScript, Go and Python; 64 more cover the editor and the host seam; 17 run in real headless Chrome on WebGL**, and `pptx`, `docx_viewer`, `datagrid` and the EVG toolbar suites are unchanged by the extractions (33 / 220 / 100 / 125 / 25, 244, 113, 23)
2313
3357
 
2314
3358
  - **A tool that says what this reader does not understand, and then most of the answer** — every fixture in the pptx gallery was written by the same hand as the reader, so every fixture is understood by construction; two decks from outside turned up six defects in an afternoon, all of them the same shape: an element walked straight past, drawing nothing and saying nothing. `npm run pptx:audit -- deck.pptx` walks every part of a package through the reader's OWN parser and reports what it never looks at, most-used first, in **two** lists — *known and deliberately not drawn* (3-D, embedded fonts, hyperlinks, per-script font fallbacks, animation beyond the build) and *UNREAD, nobody decided about these*. The difference between those lists is the difference between a decision and an oversight, and it turns "the slide looks wrong" into a work order; `npm run pptx:audit:check` runs it over every fixture and fails when one says something nobody has decided about. Pointed at the two decks the list was 24 and 39 kinds long, and it was mostly saying one thing: **text is inherited, not stated**. DrawingML states nine levels of list style, `a:lvl1pPr` … `a:lvl9pPr`, and this reader read the first — and only from the shape itself — so every sub-bullet in every real deck came out in the top level's size, colour and indent. A list style is nine levels now, every field paired with a "was this stated" flag because they are MERGED down a chain: master `p:txStyles` → the master's own placeholder → the layout's placeholder → the shape → the paragraph. The order cost a defect to get right: a deck built by anything but PowerPoint leaves `p:txStyles` as a generic black nobody meant and puts the real typography on the master's PLACEHOLDER, which sits above it — and the placeholder merge was applying its default run properties to every run before the level each paragraph sits at had been consulted, which marked them all as sized and coloured. Beside it: **`a:normAutofit`**, which is PowerPoint writing down how far it already shrank the text to make it fit (ignoring it is an overflow with a known cause); **bullets with their own colour, size and face**, and a layout that uses the deck's own `marL`/`indent` rather than a level's worth of invented indent; a paragraph's own `<a:buNone/>` and a stated `marL="0"` treated as decisions rather than silences; **`a:highlight`**, the colour behind a run; and the same fallback gap the emoji had one block down the codepoint chart — **a bullet is a geometric shape** (● ○ ■ ▪) that the text face does not have, so every list drew a column of empty boxes until Noto Sans joined the fallback pool. The two decks now report 9 and 22 kinds, **all deliberate, nothing unread**. `31-inherited-text.pptx` pins it down and round-trips byte for byte, which needed the writer to state what the levels resolved to — there is no master left to inherit from — and turned up one more instance of a defect this repository keeps meeting: `to_int` FLOORS, so `emu(-18pt)` biased and floored a negative twice and wrote -228601 EMU where the file said -228600, moving every hanging bullet by a fraction of a pixel
2315
3359
 
@@ -2346,7 +3390,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2346
3390
 
2347
3391
  - **A dot asks what the thing can do** — member completion, with a guess at the type behind it. Typing `.` opens the list with no prefix (because `body.` is already a question) and the answer comes from reading BACKWARDS for where the receiver was declared and classifying what is on the right of the `=`: an array literal offers `push` / `map` / `length`, a quoted string offers `toUpperCase` / `split`, `sheetRows(…)` is known to return rows so its members are an array's, `usedRows(…)` offers `toFixed`, an object literal offers **its own keys**, and `rows[i]` takes one step down through the subscript. There is no type checker and there is not going to be one on a keystroke — what there is, is the four ways a value gets its shape in a report script, which covers nearly every line anyone writes; when none of them matches the guess is `any` and the list is everything, which is what a type checker says about `any` too and is better than an empty popup. The member lists are **the methods ComponentEngine actually implements**, read off the engine rather than off the JavaScript standard: offering `.flatMap()` because JavaScript has it would be offering to write a line that cannot run. The Ranger plugin answers `this.` from the file — every `fn`, `sfn` and `def` in it, labelled `method`, `static` and `field` — because in a language with no imports to chase, the file is the scope. 121 unit checks and 83 in the browser
2348
3392
  - **The editor suggests, and its clipboard stopped lying** — autocomplete is the third question a language plugin answers, beside colours and problems: `complete(lines, line, col)` returns labelled candidates, two letters open the list, **Ctrl+Space** opens it whatever the prefix, arrows choose, Enter or Tab accepts and Escape closes. Accepting **replaces the word being typed** rather than appending to it. What is in the list is the plugin's opinion and the order is the whole of it: for TSX the **workbook API first** (`sheetRows`, `formatNumber`, `param`), then keywords, then every word already in the file — a report script is mostly the first and third and almost never `instanceof`; for Ranger the same shape with operators first. Neither plugin parses to answer, because `complete` runs on a keystroke. The popup is drawn into the display list like everything else, so WebGL and OpenGL both get it without knowing what a completion is, and it is **mirrored into the DOM** for assistive technology — the textarea becomes a `combobox` with `aria-expanded` and `aria-activedescendant`, the options live in a hidden `listbox`, and the live region says "6 suggestions, sheetRows selected". **Copy and cut now answer from the model.** Letting the hidden textarea serve Ctrl+C was right for a selection inside one line — the mirror holds exactly that — and silently wrong for every other: three selected lines copied one, and cut copied without deleting. `selectionText()` and `cutSelection()` come from the buffer now, the chords are let through to the browser so `copy` / `cut` / `paste` fire on the textarea (which is the only way to reach the system clipboard without a permission), and `metaKey` counts as well as `ctrlKey`, which is what made it look broken on macOS. Also: double click selects a word and triple click the line, through `EditorWord`, so a word means the same thing to the mouse as it does to Ctrl+arrow. And the header's **fps meter is gone** — the page deliberately does not redraw when nothing changed, so a frames-per-second reading of an idle editor is 2 and means nothing; the numbers worth watching (lines, tokens, lex and check times, draw commands) are in the status bar and come from the editor rather than from the loop. 102 unit checks and 77 in the browser
2349
- - **A click handed the keyboard back, and the page redrew for nothing** — two defects a real browser session found that 52 passing checks did not. **Clicking into the code editor and then typing did nothing at all**: the browser moves focus on mousedown *after* the handler runs, and the canvas is deliberately not focusable (it is `aria-hidden`; the focusable element is the textarea that mirrors the caret's line), so the default action took the keyboard straight off it and gave it to `<body>`. One `preventDefault` on `pointerdown` fixes it — and the reason no test caught it is that **every test reached the editor with the keyboard**, by Tab or a programmatic focus, and none had ever clicked; `keyboard.mjs` now opens with a real mouse click and types after it. Second, the page ran at **4 fps** because the animation loop called `scene()` on every frame — build the display list, walk it, serialize it to JSON, ~1.4 ms — and then compared the string with the last one and threw it away. The loop now asks `revision()` first, a few string joins over the document version, the caret, the selection, the scroll line and the blink phase, and builds a scene only when that changed: idle went from 60 rebuilds a second to **2**, which is the caret, and which is why the blink now comes off the clock rather than off a frame counter. Third, and this one is everyone's: a Chrome profile put **160 ms — 36% of the frame — in `getShaderParameter`**, because `renderDisplayList` compiled and linked both of its shader programs on *every call*, and asking for `COMPILE_STATUS` is synchronous — it makes the CPU wait for a compile the driver was entitled to defer. `gallery/evg/gl/evg-webgl.js` caches the two programs and their attribute and uniform locations per GL context now, so the DataGrid's own WebGL page gets the same fix. A full redraw of the editor measures 2.9 ms of GL plus 1.4 ms of scene building on software rasterization in headless Chrome. Regression checks added for both: a mouse click that hands over the keyboard, and an idle second that redraws a handful of times rather than every frame (58 checks)
3393
+ - **A click handed the keyboard back, and the page redrew for nothing** — two defects a real browser session found that 52 passing checks did not. **Clicking into the code editor and then typing did nothing at all**: the browser moves focus on mousedown *after* the handler runs, and the canvas is deliberately not focusable (it is `aria-hidden`; the focusable element is the textarea that mirrors the caret's line), so the default action took the keyboard straight off it and gave it to `<body>`. One `preventDefault` on `pointerdown` fixes it — and the reason no test caught it is that **every test reached the editor with the keyboard**, by Tab or a programmatic focus, and none had ever clicked; `keyboard.mjs` now opens with a real mouse click and types after it. Second, the page ran at **4 fps** because the animation loop called `scene()` on every frame — build the display list, walk it, serialize it to JSON, ~1.4 ms — and then compared the string with the last one and threw it away. The loop now asks `revision()` first, a few string joins over the document version, the caret, the selection, the scroll line and the blink phase, and builds a scene only when that changed: idle went from 60 rebuilds a second to **2**, which is the caret, and which is why the blink now comes off the clock rather than off a frame counter. Third, and this one is everyone's: a Chrome profile put **160 ms — 36% of the frame — in `getShaderParameter`**, because `renderDisplayList` compiled and linked both of its shader programs on *every call*, and asking for `COMPILE_STATUS` is synchronous — it makes the CPU wait for a compile the driver was entitled to defer. `lib/evg/gl/evg-webgl.js` caches the two programs and their attribute and uniform locations per GL context now, so the DataGrid's own WebGL page gets the same fix. A full redraw of the editor measures 2.9 ms of GL plus 1.4 ms of scene building on software rasterization in headless Chrome. Regression checks added for both: a mouse click that hands over the keyboard, and an idle second that redraws a handful of times rather than every frame (58 checks)
2350
3394
  - **The SQL box shows the schema and offers examples written for it** — a prompt with no manual is not much help, and the one thing a person needs before writing a `SELECT` is the names of the tables and their columns. The box now lists them (`sales(id*, region, country, month, revenue, units)`, a star marking a key column, which is what decides whether a sheet built from the query can be edited) and offers five one-click examples generated from that schema — first rows, a `GROUP BY` over a text column and a numeric one, a `LIKE` filter, a `ORDER BY … DESC LIMIT`, and a `COUNT(*)` — so each of them runs as it stands instead of being a template to adapt. Both are asked of the session every time the box opens, so they are right after a reconnect too. A test parses every generated example and checks it names a table of this database. Dialog labels are also trimmed to the panel now (`EVGTextFit.fitLabel`, the same cut the input already used): a status line or a column list is text somebody else wrote, and it must not run out of the window
2351
3395
 
2352
3396
  - **The chart API runs in the browser, at `/evg/chart-api/`, and JavaScript calls it directly** — every other page in the EVG gallery is drawn ahead of time and published as a file, which proves the API built a chart *once, on a build machine, in Node*, and says nothing about whether the thing that drew it still runs where a reader is. `gallery/vela/tools/vela_chart_web.rgr` compiles the API to a browser bundle that publishes the compiled classes themselves — `VlChart`, `VlChartMark`, `VlDataset` — so the page's **primary language is JavaScript** and `chart.bar().x("region")` in the editor is those classes' own methods with no binding layer in between (the page counts the calls with a `Proxy` and hands the finished chart back to `VelaChartApi.draw`). **Ranger is the second tab**, through a small dispatcher over the same methods, and **the same chart in either language draws the same SVG byte for byte** — which is the whole claim the page makes, so the browser check asserts it for all eight presets rather than assuming it. The rest of the entry describes that dispatcher: `chart.bar().x("region")` calls `VlChart.bar()` and then `VlChartMark.x("region")`, the chart redraws as you edit, and the Vega-Lite those calls built and the Vega it compiles to are on the tabs beside it. Editing the **data** redraws too — the difference between a live page and a picture of one. It is an interpreter over the real methods rather than a second implementation: there is no chart type in that file and no specification is written by hand, so a name the API does not have is refused *by name* (`line 2: a chart has no method 'colour'`, `'x' needs its argument in quotes: region`, `the data has no column called 'profit'` — the last being the API's own check running client-side, because the dataset is there). `npm run showcase:api` opens it in Chromium and checks **through** the page: every preset draws a different chart, an unknown method is refused, editing the data redraws, the inferred column types are shown, the specification and the Vega are both present, and a page error or console error fails the run — **22 checks**. One thing worth knowing for anyone shipping a Ranger bundle to a browser: the compiler writes a `#!/usr/bin/env node` shebang, and a `<script>` is not a shell — left in, the page loads nothing and says nothing.
@@ -2359,7 +3403,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2359
3403
 
2360
3404
  - **A connection window for the database-backed workbook (Ctrl+D)** — until now the only sign of which engine a sheet was talking to was a line in the status bar. The window shows engine, data source and table as editable fields, and under them what was actually negotiated: rows loaded, the key columns that make the sheet writable (or the reason it is read only), what the engine can do for itself (`DBCapabilities.describe()`) and what Ranger had to do instead (`DBSession.lastFallback`). Connecting is the **host's** part, like opening a file picker: **Connect** leaves a request behind (`takeDbRequest()` → `"connect"` with driver, DSN and table) and the host opens the engine and calls `bindDatabase`; `web/serve.mjs` services it with `GridDbLauncher`, and a host that ignores it keeps the sheet it already has. Typing another engine into the field and pressing Enter switches a live workbook — DuckDB to RangerDB and back — which the window smoke now drives through the same events a person sends. Demo rows are only seeded into `:memory:`: pointed at a file, a table that is not there is reported rather than invented
2361
3405
 
2362
- - **Vela has a chart API: marks and channels, called rather than written** — `gallery/vela/src/VlChart.rgr`. A program that wanted a chart used to have to write a specification out as text, which is how `gallery/datagrid` builds its twenty chart types: three hundred lines of string concatenation, quote by escaped quote. The API writes the same Vega-Lite **value** instead, and `toSpec()` hands it to `VlCompile` and `VlRuntime` exactly as pasted text is handed to them — the fluent surface is a writer of specifications and not a second implementation of anything, which is AntV G2's idea and the reason none of this can drift away from the engine. Four things follow from that and from holding the data (`VlDataset`, an ECharts-style value beside the chart rather than inside it): a **view's encoding is inherited by its marks**, so an area and the line on top of it are two marks with one set of axes and one legend, resolved at emit time into a `layer` that states every channel in full; a **channel need not say what it is** — a column of ISO dates is temporal, of numbers quantitative, of anything else nominal, read off the rows (Observable Plot's ergonomics); a **column the data does not have is an error** rather than the empty axis Vega-Lite would draw and not mention; and what the API does not cover is **written out by hand into the same specification**, where the layer that knows refuses it by name (`a bin written with 'step' is not compiled here`) instead of the API keeping its own list of what the layer below supports. Checked three ways: `tests/chart_test.rgr` builds ten charts and compares the **Vega** each compiles to against a hand-written Vega-Lite specification, then runs each one (71 checks, and two matching refusals fail rather than pass); `tools/reference/chart_api.mjs` gives the six charts of `tools/vela_chart.rgr` — a file with no specification text in it — to the **official** Vega-Lite and Vega and compares the ink to a quarter of a pixel (**6 of 6**); and `tests/run_cpp.sh` requires the same 71 checks to pass and the same six charts to come out byte for byte from a `g++` binary with no JavaScript underneath. It also has a page of its own on the published EVG showcase — **Charts, called**, `gallery/evg/showcase/pages/chart_api.tsx`, the only page there that no specification was written for: each chart is printed with the calls that built it, and those lines are read out of the generating tool's own source at the markers around each chart's calls, so the code the page shows and the code that drew the page cannot drift apart (without the source the tool refuses to write the page). Getting code onto an EVG page turned up one thing worth knowing: **JSX text is tokenised as if it were code**, so a double quote in it opens a string and `x("region")` came out as `x(region )` — a line goes on as a string *literal* in an expression container instead, which is the one place the parser keeps a quote. Design, surface and what is still missing (interaction, a host API, composition beyond a layer) in [`gallery/vela/CHART_API.md`](gallery/vela/CHART_API.md). `npm run vela:chart`, `npm run vela:showcase && npm run showcase`
3406
+ - **Vela has a chart API: marks and channels, called rather than written** — `gallery/vela/src/VlChart.rgr`. A program that wanted a chart used to have to write a specification out as text, which is how `gallery/datagrid` builds its twenty chart types: three hundred lines of string concatenation, quote by escaped quote. The API writes the same Vega-Lite **value** instead, and `toSpec()` hands it to `VlCompile` and `VlRuntime` exactly as pasted text is handed to them — the fluent surface is a writer of specifications and not a second implementation of anything, which is AntV G2's idea and the reason none of this can drift away from the engine. Four things follow from that and from holding the data (`VlDataset`, an ECharts-style value beside the chart rather than inside it): a **view's encoding is inherited by its marks**, so an area and the line on top of it are two marks with one set of axes and one legend, resolved at emit time into a `layer` that states every channel in full; a **channel need not say what it is** — a column of ISO dates is temporal, of numbers quantitative, of anything else nominal, read off the rows (Observable Plot's ergonomics); a **column the data does not have is an error** rather than the empty axis Vega-Lite would draw and not mention; and what the API does not cover is **written out by hand into the same specification**, where the layer that knows refuses it by name (`a bin written with 'step' is not compiled here`) instead of the API keeping its own list of what the layer below supports. Checked three ways: `tests/chart_test.rgr` builds ten charts and compares the **Vega** each compiles to against a hand-written Vega-Lite specification, then runs each one (71 checks, and two matching refusals fail rather than pass); `tools/reference/chart_api.mjs` gives the six charts of `tools/vela_chart.rgr` — a file with no specification text in it — to the **official** Vega-Lite and Vega and compares the ink to a quarter of a pixel (**6 of 6**); and `tests/run_cpp.sh` requires the same 71 checks to pass and the same six charts to come out byte for byte from a `g++` binary with no JavaScript underneath. It also has a page of its own on the published EVG showcase — **Charts, called**, `lib/evg/showcase/pages/chart_api.tsx`, the only page there that no specification was written for: each chart is printed with the calls that built it, and those lines are read out of the generating tool's own source at the markers around each chart's calls, so the code the page shows and the code that drew the page cannot drift apart (without the source the tool refuses to write the page). Getting code onto an EVG page turned up one thing worth knowing: **JSX text is tokenised as if it were code**, so a double quote in it opens a string and `x("region")` came out as `x(region )` — a line goes on as a string *literal* in an expression container instead, which is the one place the parser keeps a quote. Design, surface and what is still missing (interaction, a host API, composition beyond a layer) in [`gallery/vela/CHART_API.md`](gallery/vela/CHART_API.md). `npm run vela:chart`, `npm run vela:showcase && npm run showcase`
2363
3407
  - **`gallery/rangersql` — a SQL parser, generator and dialect transpiler in Ranger, and RangerDB's SQL front end.** SQLGlot-inspired rather than a port: tokenizer → parser → one common AST → generator, with a `SqlDialect` answering only the questions engines disagree on. The AST is a flat arena addressed by int (`SqlAst.nodes[45]`, not `node.parent.args["expressions"][0]`) because a tree of objects with parent pointers is pleasant on a GC target and painful on Rust, C++ and Swift; a node carries a kind, its text, flags and `(role, child)` pairs. Expressions are precedence climbing — one table of binding powers instead of term/factor/comparison/conjunction — and comments are tokens the parser hands to the node they were written beside, so `SELECT 1 /* c1 */ + 2 /* c2 */, 3 /* c3 */` round-trips exactly. Covers SELECT with joins, CTEs, set operations, subqueries, CASE, CAST, window functions, `IN`/`BETWEEN`/`LIKE`/`IS NULL`, array subscripts and JSON operators, plus INSERT/UPDATE/DELETE. Measured against **SQLGlot's own `tests/fixtures/identity.sql`** (980 statements it regenerates character for character, vendored with attribution): 519 identical, 3 differing, 458 not yet parsed — with the remaining buckets named in the README (DDL is 175 of them) and the identical count asserted as a baseline so a change that quietly parses less fails. Cross-checked against SQLGlot itself by `tools/sqlglot_oracle.py`: of the 522 statements RangerSQL parses, SQLGlot reads 520 of its outputs as the *same query*, the two exceptions being optimizer-hint comment placement. `Sql.transpile` moves a statement between SQLite, Postgres and MySQL (`IFNULL` ↔ `COALESCE`, `LIMIT 10, 20` → `LIMIT 20 OFFSET 10`, `x::INT` → `CAST(x AS INT)`, backtick quoting, no `NULLS LAST` on MySQL). **`gallery/rangerdb/src/SqlFront.rgr`** plans a parsed statement into the `QuerySpec` / `DBMutation` RangerDB already executes, so `capabilities.sqlText` is now true and the contract suite's SQL section runs on RangerDB, SQLite and DuckDB alike (65/65 each, 94 for RangerDB with its engine-internal and front-end tests); the planner refuses rather than guesses, naming what was in the way. The whole library is ordinary Ranger with no host bindings: 12/12 targets compile, and the RangerDB suite including the SQL front end passes identically on JavaScript, Python and native C++
2364
3408
  - **`gallery/rangerdb` — a database API with three engines behind it, and a DataGrid that edits through it.** The interface is a `QuerySpec` (table, columns, filter, sorts, groupBy, aggregates, offset, limit) rather than SQL text, because a `query(sql)`-only API forces every backend to own a SQL parser before it can answer anything — RangerDB would have had to be a SQL implementation before it could be a database, and the grid would be building strings to say "sort by column 3". SQL stays as an escape hatch and `SqlText.rgr` renders a spec into it for the engines that speak it, with every value travelling as a `?` parameter rather than as text. Results cross as `DataChunk`s — column vectors, a chunk at a time — which is what DuckDB's execution format already is, what a virtualised grid page wants and what a chart series is. Backends declare `DBCapabilities` and `DBSession` executes whatever they did not claim over the chunks that came back, so an engine is never asked to pretend. **RangerDB** itself is columnar and chunked: row groups of 1024, a scan that hands out the stored vectors when nothing is deleted, column pruning driven by the spec, streaming aggregation that never materialises the rows it sums, early scan stop for an unsorted `LIMIT`, tombstone deletes, and a whole-table text snapshot. Indexes, joins, a SQL front end, transactions and a WAL are listed as milestones rather than left as hidden gaps. Every call is synchronous on purpose (a spreadsheet repaints in a loop, Ranger has no generics for a `Task<T>`, and `async` is honoured by one class writer), so the asynchronous DuckDB driver is confined to a worker thread with the main thread blocking on `Atomics.wait` and collecting the reply with `receiveMessageOnPort`. One contract suite runs on all of them: `rangerdb` 53/53 + 19 engine-internal tests, `sqlite` (`node:sqlite`) 53/53, `duckdb` (optional `@duckdb/node-api`) 53/53; RangerDB's 72 also pass from the same source on JavaScript, Python and native C++, and the portable half compiles for 12/12 targets. `GridDbSource` makes a sheet out of a query and an `UPDATE` out of an edited cell — read-only without key columns, a refused value restored rather than left on screen, and sorting/filtering that re-runs the query instead of ordering the loaded page — verified over all three engines by `npm run datagrid:db:test` (93/93). **Ctrl+Q opens a SQL box inside the editor**: what is typed is parsed by RangerSQL and planned into the same `QuerySpec` a sheet built in code uses, so a typed `SELECT` is an ordinary editable database sheet (sort, filter, write-back and formulas all keep working); a statement beyond the planner is handed to the engine as text and the sheet says it is read only, and RangerDB — whose SQL is that planner — refuses it with the reason instead. The same thing is the `db.sql` command, so a host drives it over HTTP, and `datagrid:db:window:smoke` opens the box the way a person does (command, text events, Enter). Two bugs in the new code, found by the suite: binding a spec-driven sheet left the previous sheet's raw SQL in place, so the next sheet re-ran the old query; and `RangerDbBackend.lastError` was never cleared on success, so one refused statement made every later query look like a failure. Host backends now report their primary keys (`pragma_table_info` on SQLite, `duckdb_constraints()` on DuckDB), without which a sheet built from a typed query had no address to write an edit back to. `npm run datagrid:db:window` opens the ordinary WebGL editor window on a live database instead of an .xlsx — the host picks DuckDB if it is installed, SQLite if not and RangerDB if there is no host database at all, seeds a demo table when the one it is pointed at is empty, and `--db-dsn` points it at a real file; `npm run datagrid:db:window:smoke` drives that same host with no browser, posting the click/type/Enter events the browser posts and checking the scene it would have drawn. `npm run rangerdb:bench` times the same queries per engine and the README explains which columns are about the engines and which are about the API's row-at-a-time write path
2365
3409
  - **The playground shows shapes, on twelve targets** — the browser bundle always carried every writer the `rgrc` CLI has, but the UI offered three (JavaScript, Kotlin, Swift 6). The target picker now lists **JavaScript/TypeScript, Python, Go, Rust, C++, C#, Java, Kotlin, Swift 6, Dart, PHP and Scala**, each with its own highlighter, and four `shape` / `case` / `group` examples come with it — the closed variant family, exhaustive `match`, methods on a family, and group capabilities — so the point of PLAN_SHAPES.md ("one source, each target's own representation") can be seen by switching the dropdown: a tagged object on JavaScript and Python, a native `enum` on Rust and Swift, an interface on Kotlin and C#, a variant on C++. LLVM is left out because it has no lowering for shapes (`case` over a shape case fails to match argument types), and Swift 3 because the Swift 6 writer supersedes it. Two things the wider target list exposed: **Java output was empty in the playground** — the Java writer leaves the requested output file empty and writes one file per class beside it, and the reader returned that empty exact match rather than the code; it now shows every file that has content, each under its own banner. And **Scala cannot compile `RangerProcess.rgr`** at all (a `for`-loop with `continue`), so the five process examples mark it unsupported and the picker greys it out with the reason on hover instead of dropping compiler errors into the output pane. The example and target are both in the URL now (`?example=shape-value&lang=rust`)
@@ -2539,7 +3583,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2539
3583
  ### Known gaps
2540
3584
 
2541
3585
  - **`@serialize(true)` does not work for `cpp` or `rust`** — not a template gap: `systemclass JSONDataObject` and `JSONArrayObject` declare no C++ or Rust type at all, so these targets have no JSON representation to serialize into. Needs a design decision, not a template
2542
- - Unary minus, `range`, `min`, `abs(int)`, `round`, `pow` and `log` remain unimplemented; see [PLAN_OPERATORS.md](./PLAN_OPERATORS.md) §5
3586
+ - Unary minus, `range`, `min`, `abs(int)`, `round`, `pow` and `log` remain unimplemented; see [PLAN_OPERATORS.md](docs/plans/PLAN_OPERATORS.md) §5
2543
3587
 
2544
3588
  ## [3.2.1] - 2026-08-01
2545
3589
 
@@ -2608,9 +3652,9 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2608
3652
 
2609
3653
  - **`prepublishOnly` no longer runs the whole repository suite** — publishing the compiler ran all 56 test files, including the gallery, game-engine and native-toolchain suites. Those need SDL2, `g++`, Cannon and game fixtures that ship with neither the repo nor the package, so `npm publish` failed on the publisher's machine for reasons unrelated to the compiler (missing `SDL2/SDL.h`, `gallery/game_engine/games/ylos/index.tsx` and `physics_race/index.tsx` are absent from the repository entirely). `prepublishOnly` and `.github/workflows/publish.yml` now run `npm run test:publish` — 44 files, 355 tests, ~90s — covering parsing, type checking and code generation for every target backend. `npm test` still runs everything, and `ci.yml` is unchanged.
2610
3654
 
2611
- - **`build:dist` now includes `build:dist:module`** — `dist/api.js` was not rebuilt by the release build, which is how it drifted behind `bin/output.js`
3655
+ - **`build:dist` now includes `build:dist:module`** — `dist/api.js` was not rebuilt by the release build, which is how it drifted behind `dist/rgrc.js`
2612
3656
 
2613
- - **`dist/api.js` rebuilt from current sources** — the committed programmatic-API bundle predated several compiler fixes, so `require("ranger-compiler")` shipped older behaviour than the `rgrc` CLI. `scripts/patch-chain-desugar.js` now patches `dist/api.js` as well as `bin/output.js`, and `build:dist:module` runs it after `tsc`
3657
+ - **`dist/api.js` rebuilt from current sources** — the committed programmatic-API bundle predated several compiler fixes, so `require("ranger-compiler")` shipped older behaviour than the `rgrc` CLI. `scripts/patch-chain-desugar.js` now patches `dist/api.js` as well as `dist/rgrc.js`, and `build:dist:module` runs it after `tsc`
2614
3658
 
2615
3659
  ## [3.1.1] - 2026-06-23
2616
3660
 
@@ -2641,13 +3685,13 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2641
3685
 
2642
3686
  ### Added
2643
3687
 
2644
- - **`ProcessUiHost` notify suppress** — `beginSuppressUiNotify` / `endSuppressUiNotify` / `isUiNotifySuppressed` for batching parent↔child sync without re-entrant UI notify loops ([PROCESS_UI_NOTIFY.md](PROCESS_UI_NOTIFY.md))
3688
+ - **`ProcessUiHost` notify suppress** — `beginSuppressUiNotify` / `endSuppressUiNotify` / `isUiNotifySuppressed` for batching parent↔child sync without re-entrant UI notify loops ([PROCESS_UI_NOTIFY.md](docs/plans/process/PROCESS_UI_NOTIFY.md))
2645
3689
  - **Process view DTO regression fixture** — [tests/fixtures/process_view_dto_assign.rgr](tests/fixtures/process_view_dto_assign.rgr) (cross-class field assignment with method call on RHS)
2646
- - **Docs** — [PROCESS_UI_NOTIFY.md](PROCESS_UI_NOTIFY.md), [PROCESS_UI_VIEW_MODELS.md](PROCESS_UI_VIEW_MODELS.md); README `@process` quick start
3690
+ - **Docs** — [PROCESS_UI_NOTIFY.md](docs/plans/process/PROCESS_UI_NOTIFY.md), [PROCESS_UI_VIEW_MODELS.md](legacy/docs/PROCESS_UI_VIEW_MODELS.md); README `@process` quick start
2647
3691
 
2648
3692
  ### Fixed
2649
3693
 
2650
- - **Parser: assignment RHS method calls** — `row.field = this.helper(index)` no longer splits the call into invalid `=` operands ([PROCESS_UI_VIEW_MODELS.md](PROCESS_UI_VIEW_MODELS.md)); fix in [compiler/ng_RangerFlowParser.rgr](compiler/ng_RangerFlowParser.rgr) (`repairAssignMethodCallRhs`)
3694
+ - **Parser: assignment RHS method calls** — `row.field = this.helper(index)` no longer splits the call into invalid `=` operands ([PROCESS_UI_VIEW_MODELS.md](legacy/docs/PROCESS_UI_VIEW_MODELS.md)); fix in [compiler/ng_RangerFlowParser.rgr](compiler/ng_RangerFlowParser.rgr) (`repairAssignMethodCallRhs`)
2651
3695
 
2652
3696
  ### Changed
2653
3697
 
@@ -2719,7 +3763,7 @@ engine, Mermaid, Figma, CodeGraph, Rave — and is not in the npm tarball.
2719
3763
  - **Kotlin int/int division** — Casts operands with `.toDouble()` to avoid integer truncation (`compiler/Lang.rgr`)
2720
3764
  - **Kotlin `open fun` warnings** — `open` modifier now only emitted when a class actually has subclasses (`compiler/ng_RangerKotlinClassWriter.rgr`)
2721
3765
  - **TypeScript `instanceof` with structural types** — `typeof` in `case` context no longer emits `instanceof Record<string,any>` for mapped types; collapses to `Object`/`Array` at runtime (`compiler/ng_LiveCompiler.rgr`)
2722
- - **npm package bin path** — Changed from `bin/output.js` to `dist/rgrc.js` so the published package contains a valid binary
3766
+ - **npm package bin path** — Changed from `dist/rgrc.js` to `dist/rgrc.js` so the published package contains a valid binary
2723
3767
 
2724
3768
  ### Changed
2725
3769