@qoretechnologies/reqraft 0.10.54 → 0.10.55-pr.115.g24a94bf

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 (292) hide show
  1. package/.claude/CLAUDE.md +25 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
  4. package/README.md +1 -1
  5. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  6. package/design/FORM_ENGINE_FIELD_MIGRATION.md +155 -1
  7. package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
  8. package/design/SMART_EDITOR_UX.md +21 -0
  9. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  10. package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
  11. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  12. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
  13. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  14. package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
  15. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  16. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  17. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  18. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  19. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  20. package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
  21. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  22. package/dist/components/dpqlEditor/dpqlTags.js +5 -1
  23. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  24. package/dist/components/dpqlEditor/index.d.ts +4 -0
  25. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  26. package/dist/components/dpqlEditor/index.js +5 -1
  27. package/dist/components/dpqlEditor/index.js.map +1 -1
  28. package/dist/components/dpqlEditor/types.d.ts +42 -2
  29. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  30. package/dist/components/dpqlEditor/types.js +0 -4
  31. package/dist/components/dpqlEditor/types.js.map +1 -1
  32. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  33. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  34. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  35. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  36. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  37. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  38. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  39. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  40. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  41. package/dist/components/form/engine/CompactRow.js +192 -45
  42. package/dist/components/form/engine/CompactRow.js.map +1 -1
  43. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  44. package/dist/components/form/engine/CompactToolbar.js +5 -2
  45. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  46. package/dist/components/form/engine/FormEngine.d.ts +160 -12
  47. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  48. package/dist/components/form/engine/FormEngine.js +380 -78
  49. package/dist/components/form/engine/FormEngine.js.map +1 -1
  50. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  51. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  52. package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
  53. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  54. package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
  55. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  56. package/dist/components/form/engine/OptionFieldMessages.js +73 -12
  57. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  58. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  59. package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
  60. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  61. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  62. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  63. package/dist/components/form/engine/readFirst.d.ts +42 -1
  64. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  65. package/dist/components/form/engine/readFirst.js +74 -7
  66. package/dist/components/form/engine/readFirst.js.map +1 -1
  67. package/dist/components/form/engine/rendererTypes.d.ts +13 -0
  68. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  69. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  70. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  71. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  72. package/dist/components/form/engine/rowMenuContext.js +103 -0
  73. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  74. package/dist/components/form/engine/typeChoices.d.ts +42 -0
  75. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  76. package/dist/components/form/engine/typeChoices.js +31 -0
  77. package/dist/components/form/engine/typeChoices.js.map +1 -0
  78. package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
  79. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
  80. package/dist/components/form/expressions/DpqlRendering.js +53 -0
  81. package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
  82. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  83. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  84. package/dist/components/form/expressions/ExpressionField.js +203 -45
  85. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  86. package/dist/components/form/expressions/argumentPresence.d.ts +84 -0
  87. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  88. package/dist/components/form/expressions/argumentPresence.js +217 -0
  89. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  90. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  91. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  92. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  93. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  94. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  95. package/dist/components/form/expressions/builder/index.js +21 -65
  96. package/dist/components/form/expressions/builder/index.js.map +1 -1
  97. package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
  98. package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
  99. package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
  100. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  101. package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
  102. package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
  103. package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
  104. package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
  105. package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
  106. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
  107. package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
  108. package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
  109. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  110. package/dist/components/form/expressions/mockExpressions.js +8 -4
  111. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  112. package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
  113. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  114. package/dist/components/form/expressions/renderExpressionToText.js +95 -19
  115. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  116. package/dist/components/form/expressions/types.d.ts +7 -0
  117. package/dist/components/form/expressions/types.d.ts.map +1 -1
  118. package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
  119. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  120. package/dist/components/form/expressions/useRenderExpression.js +50 -126
  121. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  122. package/dist/components/form/fields/Field.d.ts.map +1 -1
  123. package/dist/components/form/fields/Field.js +3 -1
  124. package/dist/components/form/fields/Field.js.map +1 -1
  125. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  126. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  127. package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
  128. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  129. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  130. package/dist/components/form/fields/auto/AutoFormField.js +91 -28
  131. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  132. package/dist/components/form/fields/date/Date.d.ts +1 -1
  133. package/dist/components/form/fields/date/Date.d.ts.map +1 -1
  134. package/dist/components/form/fields/date/Date.js +16 -2
  135. package/dist/components/form/fields/date/Date.js.map +1 -1
  136. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  137. package/dist/components/form/fields/long-string/LongString.js +14 -2
  138. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  139. package/dist/components/form/fields/rich-text/RichText.d.ts +35 -2
  140. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  141. package/dist/components/form/fields/rich-text/RichText.js +153 -12
  142. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  143. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  144. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  145. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  146. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  147. package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
  148. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  149. package/dist/components/form/fields/template/TemplateField.js +606 -58
  150. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  151. package/dist/components/form/index.d.ts +1 -0
  152. package/dist/components/form/index.d.ts.map +1 -1
  153. package/dist/components/form/index.js +1 -0
  154. package/dist/components/form/index.js.map +1 -1
  155. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  156. package/dist/components/smartEditor/SmartEditor.js +29 -8
  157. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  158. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
  159. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
  160. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
  161. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
  162. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  163. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  164. package/dist/components/smartEditor/useLspSession.js +16 -2
  165. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  166. package/dist/helpers/dpqlDetection.d.ts +58 -0
  167. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  168. package/dist/helpers/dpqlDetection.js +164 -0
  169. package/dist/helpers/dpqlDetection.js.map +1 -0
  170. package/dist/helpers/optionUiTypes.d.ts +19 -3
  171. package/dist/helpers/optionUiTypes.d.ts.map +1 -1
  172. package/dist/helpers/optionUiTypes.js +20 -1
  173. package/dist/helpers/optionUiTypes.js.map +1 -1
  174. package/dist/helpers/options.d.ts +37 -0
  175. package/dist/helpers/options.d.ts.map +1 -1
  176. package/dist/helpers/options.js +78 -1
  177. package/dist/helpers/options.js.map +1 -1
  178. package/dist/helpers/templateItems.d.ts +47 -0
  179. package/dist/helpers/templateItems.d.ts.map +1 -0
  180. package/dist/helpers/templateItems.js +88 -0
  181. package/dist/helpers/templateItems.js.map +1 -0
  182. package/dist/helpers/templateText.d.ts +37 -0
  183. package/dist/helpers/templateText.d.ts.map +1 -0
  184. package/dist/helpers/templateText.js +64 -0
  185. package/dist/helpers/templateText.js.map +1 -0
  186. package/dist/helpers/templates.d.ts +30 -12
  187. package/dist/helpers/templates.d.ts.map +1 -1
  188. package/dist/helpers/templates.js +91 -33
  189. package/dist/helpers/templates.js.map +1 -1
  190. package/dist/helpers/validations.d.ts +1 -1
  191. package/dist/helpers/validations.d.ts.map +1 -1
  192. package/dist/helpers/validations.js +68 -12
  193. package/dist/helpers/validations.js.map +1 -1
  194. package/dist/hooks/useArgSchema.d.ts +2 -1
  195. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  196. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  197. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  198. package/dist/hooks/useFetch/useFetch.js +47 -9
  199. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  200. package/dist/hooks/useTemplates.d.ts +20 -0
  201. package/dist/hooks/useTemplates.d.ts.map +1 -1
  202. package/dist/hooks/useTemplates.js +34 -2
  203. package/dist/hooks/useTemplates.js.map +1 -1
  204. package/dist/index.d.ts +2 -1
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +5 -1
  207. package/dist/index.js.map +1 -1
  208. package/dist/stories/storyNetwork.d.ts +36 -0
  209. package/dist/stories/storyNetwork.d.ts.map +1 -0
  210. package/dist/stories/storyNetwork.js +82 -0
  211. package/dist/stories/storyNetwork.js.map +1 -0
  212. package/dist/utils/fetch.d.ts +151 -1
  213. package/dist/utils/fetch.d.ts.map +1 -1
  214. package/dist/utils/fetch.js +256 -44
  215. package/dist/utils/fetch.js.map +1 -1
  216. package/dist/utils/lspClient.d.ts +6 -0
  217. package/dist/utils/lspClient.d.ts.map +1 -1
  218. package/dist/utils/lspClient.js +62 -11
  219. package/dist/utils/lspClient.js.map +1 -1
  220. package/dist/utils/websocket.d.ts +6 -0
  221. package/dist/utils/websocket.d.ts.map +1 -1
  222. package/dist/utils/websocket.js +15 -7
  223. package/dist/utils/websocket.js.map +1 -1
  224. package/package.json +2 -2
  225. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  226. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  227. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  228. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  229. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  230. package/src/components/dpqlEditor/index.ts +4 -0
  231. package/src/components/dpqlEditor/types.ts +38 -2
  232. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  233. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  234. package/src/components/form/engine/CompactRow.tsx +240 -51
  235. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  236. package/src/components/form/engine/FormEngine.stories.tsx +431 -124
  237. package/src/components/form/engine/FormEngine.tsx +508 -90
  238. package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
  239. package/src/components/form/engine/OptionFieldMessages.tsx +103 -12
  240. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
  241. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  242. package/src/components/form/engine/readFirst.ts +86 -7
  243. package/src/components/form/engine/rendererTypes.ts +12 -0
  244. package/src/components/form/engine/rowMenuContext.ts +159 -0
  245. package/src/components/form/engine/typeChoices.ts +61 -0
  246. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  247. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  248. package/src/components/form/expressions/ExpressionField.tsx +266 -54
  249. package/src/components/form/expressions/argumentPresence.ts +213 -0
  250. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +31 -14
  251. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  252. package/src/components/form/expressions/builder/index.tsx +39 -81
  253. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  254. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  255. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  256. package/src/components/form/expressions/mockExpressions.ts +8 -4
  257. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  258. package/src/components/form/expressions/types.ts +7 -0
  259. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  260. package/src/components/form/fields/Field.tsx +3 -2
  261. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
  262. package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
  263. package/src/components/form/fields/auto/AutoFormField.tsx +90 -20
  264. package/src/components/form/fields/date/Date.tsx +17 -1
  265. package/src/components/form/fields/long-string/LongString.tsx +15 -3
  266. package/src/components/form/fields/rich-text/RichText.tsx +235 -30
  267. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  268. package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
  269. package/src/components/form/fields/template/TemplateField.tsx +825 -131
  270. package/src/components/form/index.tsx +1 -0
  271. package/src/components/log/Log.stories.tsx +2 -1
  272. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  273. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  274. package/src/components/smartEditor/useLspSession.ts +21 -3
  275. package/src/helpers/dpqlDetection.ts +209 -0
  276. package/src/helpers/optionUiTypes.ts +22 -0
  277. package/src/helpers/options.ts +70 -0
  278. package/src/helpers/templateItems.ts +81 -0
  279. package/src/helpers/templateText.ts +92 -0
  280. package/src/helpers/templates.ts +94 -41
  281. package/src/helpers/validations.ts +79 -24
  282. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  283. package/src/hooks/useFetch/useFetch.tsx +53 -7
  284. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  285. package/src/hooks/useTemplates.ts +41 -0
  286. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  287. package/src/index.tsx +14 -1
  288. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  289. package/src/stories/storyNetwork.ts +92 -0
  290. package/src/utils/fetch.ts +311 -21
  291. package/src/utils/lspClient.ts +68 -11
  292. package/src/utils/websocket.ts +20 -9
package/.claude/CLAUDE.md CHANGED
@@ -167,6 +167,7 @@ yarn build:test # Type-check without emit
167
167
 
168
168
  - `yarn precheck` runs: lint → test → build:test:prod
169
169
  - `pre-push` hook enforces: `build:test:prod`, `lint`, `test`
170
+ - CI (`.github/workflows/tests.yml`) runs precheck, build and the full story suite for PRs into `develop` **and** for every push to `develop`. GitHub skips `pull_request` workflows while a PR has merge conflicts, so a conflicting PR gets no test runs until it is rebased or merged up
170
171
  - Branch naming: always start with the issue number, e.g. `feature/49_pooled-connections`
171
172
 
172
173
  ### Versioning — every PR to `develop` MUST bump the version
@@ -201,6 +202,26 @@ yarn build:test # Type-check without emit
201
202
  - Stories are co-located alongside source: `src/hooks/useFetch/useFetch.stories.tsx`
202
203
  - Storybook port: **6008**
203
204
  - Story + interaction tests run on **Vitest browser mode** (Playwright/Chromium): `yarn test:stories [file-substring]`. Run `yarn install-playwright` once first.
205
+ - **Story network — mocks must follow the instance, and stories must not share connections.**
206
+ - Build every mock URL with `storyApiUrl(path)` / `storySocketUrl(path)` from
207
+ `src/stories/storyNetwork.ts`, never a hard-coded `https://hq.qoretechnologies.com:8092/…`.
208
+ Both mock layers match on host (storybook-addon-mock keys on `host + pathname`,
209
+ mock-socket on the URL minus its query), so a hard-coded host silently stops matching
210
+ when `REACT_APP_QORUS_INSTANCE` is overridden and the request goes to the real network.
211
+ - Requests components make on their own (FormEngine's `system/qorus-type-info`, the
212
+ expression catalogue `useExpressions` fetches whenever functions are allowed, the
213
+ reconnect probe `system/pid`) are mocked for every story via `GLOBAL_STORY_MOCK_DATA`
214
+ (`parameters.mockAddonConfigs.globalMockData`). Add new ones there, not per story file.
215
+ - A mock `response` must be an object, array or function — a bare number or string fails
216
+ the addon's validation and the entry is silently never matched (`TStoryMockResponse`
217
+ enforces this at compile time). The addon compares query-parameter *keys* only, so
218
+ `system?action=a&context=x` also answers `system?action=b&context=y`.
219
+ - `.storybook/preview.tsx` resets the shared LSP connections and the expression render
220
+ client before every story. Do not add per-file `ReqraftWebSocketsManager.closeAll()`
221
+ calls for LSP isolation.
222
+ - CI reaches the live hq instance with a valid token, so a story that leaks a real
223
+ request can pass in CI and hang locally. When a story passes in CI but times out on
224
+ your machine, look for an unmocked request before anything else.
204
225
  - **Visual regression via Qlip** (not Chromatic): the story run captures snapshots through the `qlip` Vitest plugin. Upload is gated on `QLIP_UPLOAD_TOKEN` (`vitest.config.ts`), so **local runs capture but never upload** — PNGs land in `qlip/screenshots/<ts>/stories/auto/<StoryId>.png` (gitignored); read the PNG to verify a visual change. CI (`.github/workflows/tests.yml`) sets the token and uploads a build for dashboard review. The full visual-change flow + the `qlip` MCP review skill live in the imported `stacks/frontend/FRONTEND.md`.
205
226
 
206
227
  ## Code Patterns & Conventions
@@ -260,6 +281,10 @@ yarn build:test # Type-check without emit
260
281
  | `query()` returning stale data | Pass `cache: false`; POST/DELETE bypass cache automatically |
261
282
  | Heartbeat noise in logs | Filter `ev.data === 'pong'` — already done inside `useReqraftWebSocket` |
262
283
  | Storage path collisions | Use `includeAppPrefix: true` in `useReqraftStorage` to namespace per app |
284
+ | Story shows FormEngine's loading skeleton / "Connecting to language server…" forever | An unmocked request or a connection inherited from an earlier story — see "Story network" above |
285
+ | "No Preview — Sorry, but you either have no stories…" in a story failure | Not a registration failure: addon-vitest injects Storybook's hidden preview body into every test page, and a failed query's DOM dump prints it. Read the assertion above the dump |
286
+ | A story run stops reporting and never times out (no failure, no output) | The page's main thread is blocked, so the in-page 30s test timer cannot fire — usually a synchronous render loop inside `act()`. Find the story with `--reporter=verbose` (the last ✓ is the one before it), then give Chromium `launchOptions: { args: ['--remote-debugging-port=9333'] }` in `vitest.config.ts` locally, enable `Debugger` over CDP BEFORE the freeze, and `Debugger.pause` once `Runtime.evaluate` stops answering to get the stack. Revert the config afterwards |
287
+ | `yarn test:stories` hangs before any test runs (the port message is incidental) | Seen when the checkout path contains `+` — e.g. an auto-named `.claude/worktrees/fix+name` — while the same tree at a `+`-free path runs normally. Use a path without `+` |
263
288
 
264
289
  ## File Reference
265
290
 
package/.tasks/INDEX.md CHANGED
@@ -58,6 +58,7 @@ needs phase 1.
58
58
  | [RENDER_EXPRESSION_LSP](./RENDER_EXPRESSION_LSP.md) | done pending user verify, branch `feature/render-expression-lsp` — server deployed + live-verified end-to-end 2026-06-10 (final gate 201/201 storybook, 268/268 jest, browser proof of server rendering) | **8th.** Server "Explain" over the LSP: new `dpql/renderExpression` in `QorusLspWebSocketHandler.qc` + `useRenderExpression` swap. Closes EXPRESSION_BUILDER_REPORT Phase D. Design: `design/RENDER_EXPRESSION_TRANSPORT.md`. |
59
59
  | [FIELD_STACK_REPORT](./FIELD_STACK_REPORT.md) | done pending user verify (uncommitted) — final gate 234/234 storybook (incl. 37 ported Template/Auto plays), 268/268 jest | **9th.** Verbatim re-port of the IDE's `Field/template.tsx` + `Field/auto.tsx` (the operand layer under the ported builder) + their IDE stories, replacing the from-scratch TemplateField/AutoFormField; closes the `isFunction` operand-flow gap (behavioral difference #4 in EXPRESSION_BUILDER_REPORT). Supersedes the from-scratch implementation from REQRAFT_AUTO_FIELD (its stories/assertions are kept and reconciled). |
60
60
  | [BRANCH_REMEDIATION](./BRANCH_REMEDIATION.md) | done pending user verify (uncommitted) — jest 268→356, plays 241→255, lint/tsc clean | **10th.** Burn-down of the 2026-06-10 full-branch audit: tests/stories/API/comment-voice remediation + behavior fixes (Select modal-click collapse, edge-triggered Url, ReqraftLspClient rename, CI auth wiring). Ledger: gitignored `SWEEP.local.md`. |
61
+ | [STORY_NETWORK_ISOLATION](./STORY_NETWORK_ISOLATION.md) | done pending user verify, folded into `fix/per-field-templates` (PR #115), not pushed | **Standalone.** ExpressionField stories failed off-network on `develop` (passed in CI only because CI reaches the live instance): LSP connections leaking across stories, an unhandled socket give-up in `LspSharedConnection`, unmocked background requests, and mock URLs hard-coded to the hq host. |
61
62
  | [TEMPLATE_TOKEN_GRAMMAR](./TEMPLATE_TOKEN_GRAMMAR.md) | committed `0ef167c`, in PR (branch `bugfix/template-token-grammar`) — user-verified 2026-08-27; 13/13 unit, 21/21 template plays, qlip PNGs read | **11th (bugfix).** Braced `$data:{…}` state-output refs rehydrate as raw text: canonical token grammar + strict whole-token check + TemplateField renders whole-token values as the picker chip. Twin fix applied in qorus-ide the same day; ships as a normal per-PR patch release (`0.10.37`). |
62
63
  | [EXPRESSION_FIELD_SEAMS](./EXPRESSION_FIELD_SEAMS.md) | committed `f5fb2ee`, pushed (branch `bugfix/116_expression-field-seams`) — user-verified 2026-09-07; PR not yet opened | **12th (bugfix).** The `extraActions` seam stopped one hop short of the `ExpressionField` shell hosts mount, and the builder's operand `TemplateField` mounts dropped it again — qorus-ide lost its AI-assist button on every expression card when it adopted the shell ([#116](https://github.com/qoretechnologies/toolkit-react/issues/116)). Forward it through both hops; pin with a nested-operand story; ships as a per-PR patch (`0.10.51`; re-based onto `4860e6e` after #114 published 0.10.50). |
63
64
  | [EXPORT_EXPRESSION_BUILDER](./EXPORT_EXPRESSION_BUILDER.md) | committed `0eea9e7`, pushed, PR open (branch `feature/119_export-expression-builder`) | **13th (surface fix).** The barrel exported the `ExpressionField` shell but not the bare `ExpressionBuilder`, so a host mirroring the library's root/nested rule had to deep-import `dist/…/builder` ([#119](https://github.com/qoretechnologies/toolkit-react/issues/119)). Export + a barrel test; ships as `0.10.53`. |
@@ -0,0 +1,98 @@
1
+ # Story network isolation — ExpressionField stories fail off-network
2
+
3
+ **Status:** done pending user verify — folded into `fix/per-field-templates`
4
+ (PR #115) so every open reqraft fix ships in one PR; not yet pushed.
5
+ Originally built and verified on `develop` (stories 479/479, unit 930/930,
6
+ off-network), then re-verified on the combined branch.
7
+
8
+ ## Symptom
9
+
10
+ `yarn test:stories ExpressionField` failed 6 of 8 on `develop` locally, while the
11
+ same file passed in CI (run 34250300001, 2026-09-08). Siblings failed the same
12
+ way: FormEngineRemote 11/11, AutoFormField 1, SchemaDefinitionField 2,
13
+ useWebsocket 3, TemplateField 1.
14
+
15
+ ## Root causes
16
+
17
+ 1. **An LSP connection outlived the story that opened it.** `useRenderExpression`
18
+ holds the endpoint's `LspSharedConnection` for the page lifetime. `Default`
19
+ (no mock LSP) dialled the real instance; `Empty` / `Text Mode` then joined that
20
+ connection's never-settling handshake, so their mock LSP was never dialled
21
+ ("Connecting to language server…" forever). Each passes when run alone.
22
+ 2. **The shared connection had no way out of a dead socket** (product bug). No
23
+ `onReconnectFailed` handling: a first handshake whose socket gave up stayed
24
+ pending forever for every later client; one that had completed kept reporting
25
+ the dead socket as ready. `close()` also abandoned an in-flight handshake, and
26
+ `release()` could evict a connection that had replaced its own after a reset.
27
+ 3. **Requests components make on their own were unmocked.** FormEngine gates on
28
+ `system/qorus-type-info`; TemplateField's "Use Expression" waits on the
29
+ expression catalogue; every `ReqraftWebSocket` reconnect awaits a `system/pid`
30
+ probe. Against an unreachable instance none settles — the form stays a
31
+ skeleton, the menu entry never appears, the socket never reconnects or gives
32
+ up. CI's live instance answered all three, which is why CI stayed green.
33
+ 4. **Mock URLs hard-coded the hq host.** Both mock layers match on host, so
34
+ overriding `REACT_APP_QORUS_INSTANCE` silently disabled every mock.
35
+
36
+ ## Fix
37
+
38
+ - `src/utils/lspClient.ts` — give-up / close settle the handshake and forget the
39
+ socket; retry paths guarded by identity; `release()` only evicts itself.
40
+ 4 new unit tests + the disconnect-mid-handshake test now asserts the rejection.
41
+ - `src/stories/storyNetwork.ts` — `STORY_QORUS_INSTANCE`, `storyApiUrl`,
42
+ `storySocketUrl`, `GLOBAL_STORY_MOCK_DATA` (types catalogue, empty expression
43
+ catalogue, reconnect probe),
44
+ `TStoryMockResponse` (the addon silently ignores non-object responses).
45
+ - `.storybook/preview.tsx` — `mockAddonConfigs.globalMockData`, per-story LSP
46
+ reset in a project `beforeEach`; `.storybook/vitest.setup.ts` mirrors the
47
+ addon's global + story merge.
48
+ - All mock URLs moved to the helpers; FormEngine's duplicate types mock and
49
+ DpqlEditor's ad-hoc `closeAll()` removed.
50
+
51
+ ### Reconciled with `fix/per-field-templates`
52
+
53
+ - That branch had already met the leak from the fixture side:
54
+ `createMockLspServer` reset the shared connections on create and close. The
55
+ project-level `beforeEach` covers the same ground plus stories that never
56
+ create a mock server (and the render client), so the fixture resets were
57
+ dropped in favour of the one mechanism.
58
+ - Its `query()` strips a path's leading slash and uses a `noApiPrefix` url
59
+ verbatim; `buildReqraftApiUrl` follows those rules, so the doubled-slash mock
60
+ spellings are gone.
61
+ - **A page-freezing render loop in the row menu** (from `a21640b`), reachable
62
+ only once FormEngine rendered offline: `FormEngine › Compact Expressions`
63
+ froze the tab and hung the whole suite with no output. The row kept ONE
64
+ `{key, items}` slot, and an expression's operands — separate TemplateFields
65
+ in the same row — published different keys into it, each overwrite
66
+ re-rendering the row and both operands. The same slot also kept an unmounted
67
+ editor's handlers when a reopened row offered the same key. Fixed in
68
+ `rowMenuContext.ts` (per-publisher registration via `useId`, withdrawn on
69
+ unmount, clicks delegated to the latest handler) and `TemplateField` (claims
70
+ the channel and hides it from what it renders, so operands keep their own
71
+ menu). Unit tests reproduce the loop and both stale-handler cases.
72
+ - `ExpressionField › Field Menu Stays In The Toolbar` had gone stale on that
73
+ branch: `a21640b` moved a row field's menu into the row's ⋮ (one menu per
74
+ control) after the story was written. The story now asserts that single
75
+ menu — the row's `.options-readfirst-more`, with no `.template-more` beside
76
+ it — still sits level with the toolbar.
77
+
78
+ ## Not causes (checked)
79
+
80
+ - "No Preview" in failure output: addon-vitest injects Storybook's hidden preview
81
+ body into every test page; a failed query's DOM dump prints it.
82
+ - A `+` in the checkout path hangs `yarn test:stories` before any test runs
83
+ (mechanism not traced); unrelated to the stories.
84
+
85
+ ## Follow-ups (not done)
86
+
87
+ - `users?action=current` is still unmocked for most stories — harmless today
88
+ (`waitForStorage={false}`), but CI renders a real user while local runs render
89
+ none. Candidate for a default mock once permission-gated snapshots are checked.
90
+ - Unmocked requests still pass through to the network. Failing them loudly for
91
+ non-live stories would make this class impossible rather than documented.
92
+ - `ReqraftWebSocketsManager.closeAll()` deletes entries by `socket.url` rather than
93
+ the pool key, and closing a CONNECTING socket reports code 1006, which schedules
94
+ a reconnect. Not exercised by this fix.
95
+ - ~~`tests.yml` runs only on `pull_request`~~ — done on this branch: it now also
96
+ runs on pushes to `develop` (the push trigger had been dropped in 2021 with no
97
+ recorded reason). Conflicting PRs still get no `pull_request` runs until they
98
+ merge up.
package/README.md CHANGED
@@ -148,7 +148,7 @@ client.onNotification('qonsole/sessionStateChanged', (params) => { /* … */ });
148
148
  client.disconnect();
149
149
  ```
150
150
 
151
- Clients on the same LSP endpoint share ONE underlying WebSocket — each `ReqraftLspClient` is a per-document facade over the shared connection, multiplexed by document URI (the server keys language sessions per document), so N editors cost one socket. Auto-reconnect re-opens every document on the shared socket, 15s request timeout, request/response correlation by `id`, pending requests rejected on close. Document URIs should be opaque and client-generated — per the Qonsole LSP contract they must not contain session tokens, usernames, sandbox identifiers, or other secrets that end up in server logs.
151
+ Clients on the same LSP endpoint share ONE underlying WebSocket — each `ReqraftLspClient` is a per-document facade over the shared connection, multiplexed by document URI (the server keys language sessions per document), so N editors cost one socket. Auto-reconnect re-opens every document on the shared socket, 15s request timeout, request/response correlation by `id`, pending requests rejected on close. A `connect()` never outlives its socket: once the socket exhausts `maxReconnectTries`, a handshake still waiting rejects, and the next `connect()` — from that client or any other — dials again, so an editor opened after an outage connects once the server is back, without a page reload. Document URIs should be opaque and client-generated — per the Qonsole LSP contract they must not contain session tokens, usernames, sandbox identifiers, or other secrets that end up in server logs.
152
152
 
153
153
  ## Community
154
154
 
@@ -124,6 +124,116 @@ templates/return-type, and bind a provider only when the field supplies
124
124
  one. Worst case, text mode still parses/serializes correctly with no
125
125
  provider; only completions are reduced.
126
126
 
127
+ ## Detecting an expression typed as plain text
128
+
129
+ *Added 2026-09-06. As-built.*
130
+
131
+ An author who never opens the expression view still types DPQL into the
132
+ ordinary editor. `TemplateField` notices, and what it does next depends on
133
+ the field's own type.
134
+
135
+ **A successful parse means nothing.** This is the finding the whole design
136
+ rests on, and it is counter-intuitive enough to be worth stating plainly.
137
+ Verified against the live LSP: `dpql/parse` returns `success: true` for
138
+ `hello`, for `42`, for `2026-09-06` and for `@a`. A bare literal is a valid
139
+ DPQL program, so "does it parse?" cannot separate an expression from a
140
+ value. What separates them is the shape of the AST that comes back:
141
+
142
+ | typed text | `exp` | verdict |
143
+ |---|---|---|
144
+ | `hello`, `42`, `2026-09-06`, `@a` | `value` | a literal |
145
+ | `$local:count` | `template` | a template |
146
+ | `@a > 5`, `"a" + "b"`, `toInt("5")`, `@n LIKE "a%"` | `>`, `+`, `toInt`, `like` | an expression |
147
+
148
+ Text that is not DPQL at all is safe on its own: `hello world`, `x@y.com`,
149
+ `a - b test` and `{"a":1}` all fail to parse outright.
150
+
151
+ **Three stages, in `helpers/dpqlDetection.ts`:**
152
+
153
+ 1. `mightBeDpqlExpression` — a cheap lexical pass whose ONLY job is to keep
154
+ a server round-trip off every keystroke. It decides nothing. `-` and `/`
155
+ require surrounding spaces so a date or a path never triggers a probe.
156
+ 2. `dpql/parse` on the shared probe document (below), debounced 400 ms.
157
+ 3. `classifyTypedText` — `switch` when the text could not be a literal of
158
+ the field's declared type (the form's own `validateField` answers that),
159
+ `offer` when it could, `none` otherwise.
160
+
161
+ **Why the asymmetry.** Rewriting the string `a + b` into a concatenation on
162
+ a `string` field would destroy a value the author meant, so a type that
163
+ accepts any text is only ever OFFERED the switch. Text that could not be a
164
+ literal there was already an error the moment it was typed, so switching
165
+ costs nothing and explains the error. Every switch carries an Undo that
166
+ restores the exact text, and a declined text is never asked about again.
167
+
168
+ **Reachability.** The auto-switch only applies where the editor accepts free
169
+ text — a constrained `string` (identifier / `validation_regex`), `binary`,
170
+ `hash`. `int` and `float` render `input[type=number]`, which drops `@`, `>`
171
+ and spaces before any handler runs, so detection there is unreachable by
172
+ design of the editor and the ⋮ menu remains the way in. Pinned by a test.
173
+
174
+ **Template precedence.** `isValueTemplate` is deliberately loose (starts
175
+ with `$`, contains `:`), so `$local:count + 1` was being swallowed into
176
+ template mode with its arithmetic as dead text. A value that is not one
177
+ complete template token and carries an operator now stays in the plain
178
+ editor, where detection can reach it. A token standing alone
179
+ (`$local:count`, `$data:{1.field}`) still flips, unchanged.
180
+
181
+ **The probe document** (`components/dpqlEditor/dpqlProbe.ts`). One shared,
182
+ lazily-opened DPQL document for the whole app, reference-counted by
183
+ `useDpqlProbe` and closed when the last field using it unmounts. It is
184
+ cheap: `ReqraftLspClient` multiplexes every document over ONE socket per
185
+ endpoint and `initialize` runs once for it, so a probe is one extra
186
+ `didOpen` plus one server-side session — not a connection. It is still
187
+ shared rather than per-field, because a form with forty expression-capable
188
+ options would otherwise open forty sessions to ask the same question.
189
+
190
+ The document binds NO provider context; verified live, a context-less
191
+ `didOpen` still builds a session that `dpql/parse` answers on. It sends no
192
+ `target_type` either: detection asks whether the text is an expression, not
193
+ whether it is the right type — a mismatch is something to show the author
194
+ inside the editor, never a reason to refuse to open it. A probe never
195
+ throws; an unreachable instance simply leaves the typed text alone.
196
+
197
+ ## Return-type checking
198
+
199
+ *Added 2026-09-06. As-built.*
200
+
201
+ `dpql/parse` has always been able to answer "can this expression's result
202
+ satisfy type X?" — it takes a `target_type` and returns `inferred_type`,
203
+ `type_compatible`, `auto_coercible` and a `suggested_fix`. Nothing in the
204
+ front end ever sent one, so it never answered.
205
+
206
+ `ExpressionField` now sends the field's own type with every parse, and
207
+ shows what comes back. Three outcomes, not two:
208
+
209
+ | the expression | shown |
210
+ |---|---|
211
+ | already fits | nothing |
212
+ | converts, and the conversion is TOTAL (a number used as text) | nothing |
213
+ | converts, but the conversion may FAIL (text used as a number) | a warning, with the conversion offered |
214
+ | cannot convert at all | a danger message, with the conversion offered |
215
+
216
+ The middle row is why the server grew `coercion_may_fail`: `canAutoCoerce()`
217
+ admits both kinds, and reporting a conversion that cannot fail is what
218
+ teaches authors to ignore the reports that matter.
219
+
220
+ `auto` and `any` are never sent as a target — they accept anything, so the
221
+ answer would always be "compatible" and the round trip would buy nothing. A
222
+ field offering a CHOICE of return types sends nothing either, rather than
223
+ asking about one of them arbitrarily.
224
+
225
+ **What the field cannot decide.** Whether text really denotes a number
226
+ depends on the text, which does not exist until the expression runs. So the
227
+ warning is a warning, not a refusal, and the value is checked again
228
+ server-side at run time — see `design/test-interface-object.md` in the qorus
229
+ repo. For that to be possible the FormEngine's stored envelope carries the
230
+ field's `type` beside `is_expression` and the AST, which is the type the
231
+ author was shown.
232
+
233
+ The same analysis lives in two server handlers (the LSP's `dpql/parse` and
234
+ the Creator WS equivalent); both were changed together. Deduplicating them
235
+ is a separate cleanup.
236
+
127
237
  ## Out of scope
128
238
 
129
239
  - `server_expression_handling` server-function-vs-qorus-function
@@ -129,6 +129,158 @@ Create `src/components/form/fields/auto/AutoFormField.tsx`:
129
129
  collapsing to `'long-string'`. **This is the one behaviour change for
130
130
  existing consumers** — see Migration impact.
131
131
 
132
+ **Revised 2026-09-14 — an untyped field is typed into, never asked for a
133
+ type.** No field shows the type picker's *"Please select data type"*
134
+ anymore. It asked a question about storage ("is this Text or a Number?")
135
+ that an author writing a value often cannot answer, before they could write
136
+ anything, and it replaced a typable field with a pick-only one.
137
+
138
+ - **`AutoFormField`**, for a type that is still `auto` / `any`, renders the
139
+ long-string editor and stores what is typed as untyped — exactly what
140
+ template mode stores. The picker appears only where there is a real
141
+ choice: the schema allows several concrete types (`allowed_types`), or
142
+ the field already holds a value of a concrete type.
143
+ - **An explicit type** is chosen from the field's ⋮ menu — the per-type
144
+ entries under *Set Custom Value* (see `engine/typeChoices.ts`).
145
+ - **`TemplateField`** opens an empty untyped field that may hold a custom
146
+ value in template mode, whose editor is typable and offers the templates.
147
+ It decides that from the field's props, not from whether the template list
148
+ has arrived: on a cold load the type-filtered list does not exist at
149
+ mount, and a landing chosen from it put every such field on the picker.
150
+ - A field that may NOT hold a custom value (FormEngine passes
151
+ `allowCustomValues={false}` for `any`) still opens on its template menu —
152
+ a pick-only control because the schema made it one, not a type question.
153
+ - Emptying that editor leaves the field in template mode, where an empty
154
+ field lands. Switching to custom mode mounted a different editor in place
155
+ of the one being typed in, so the last backspace lost the cursor.
156
+
157
+ Guarded by `__tests__/untypedFieldNeverAsksForAType.test.tsx` and the
158
+ `AutoFormField` / `TemplateField` stories (`Empty`, `ViaFormEngine`,
159
+ `AutoComponent`, `EmptyAnyOpensOnTemplates`).
160
+
161
+ **Revised 2026-09-15 — template mode draws references as named chips.**
162
+ Template mode's typable editor (a string or untyped field that may also hold
163
+ a custom value) is `RichTextFormField` with `valueFormat: 'text'`, not a
164
+ textarea. The field still stores the plain string; each template reference in
165
+ it (`TEMPLATE_TOKEN_SOURCE`) is drawn as a chip, so a chosen template reads as
166
+ the name it was chosen by — in the Visual builder's operands and in every
167
+ string field that takes templates. A textarea spelled it `$local:name`.
168
+
169
+ - The chip's label is `templateChipLabel`: the catalogue's name for the
170
+ reference, else the reference as the DPQL Text view labels it
171
+ (`local: name`), so one reference reads alike in both views. Its hover and
172
+ colour are the collapsed row's (`ReadOnlyTemplateTag`).
173
+ - The editor keeps its own document and rebuilds it only when the stored
174
+ string changes from outside: rebuilding from the echo of what was typed
175
+ would hand the editor a new tree per keystroke, which moves the cursor.
176
+ - `string` holds one line: Enter adds none, and a pasted break is flattened.
177
+ - A value that is exactly one braced reference (`$data:{…}`, machine-written)
178
+ keeps its pick-only chip selector, unchanged.
179
+ - Only a value that STARTS with a reference enters template mode, so
180
+ `AutoFormField`'s text editor — `string`, `long-string` and untyped — is the
181
+ same chip editor whenever the field offers templates: text like
182
+ `Interface $local:id failed` reads with its references named too. `data`
183
+ and `binary` keep the textarea (encoded content), as does a field with no
184
+ templates to offer. Guarded by `__tests__/textFieldDrawsTemplateChips.test.tsx`
185
+ and the `TemplateField` stories *Template References In Text* (+ *On Phone*).
186
+
187
+ This reverses the 2026-08-27 choice (`85ba6ec`) to keep plain tokens as raw
188
+ text: that review rejected a *pick-only* chip because it could not be typed
189
+ into, and this editor is both. Conversion lives in `helpers/templateText.ts`
190
+ (`__tests__/templateText.test.ts`, `__tests__/richTextTextValue.test.tsx`).
191
+
192
+ **Revised 2026-09-16 — a field offers a template list only when it has one,
193
+ and template mode has a way out.** Two rules about the menu, both reported
194
+ from the stories:
195
+
196
+ - **No empty menu.** `TemplateField` hands its editor `undefined` — not an
197
+ empty list — when there is nothing to offer: the field's schema does not
198
+ allow templates (a `FormEngine` option without `supports_templates`), or the
199
+ type filter emptied the catalogue. An editor GIVEN a list draws the control
200
+ that opens it, so a list with no items opened a menu onto nothing. Guarded
201
+ by `__tests__/noEmptyTemplateMenu.test.tsx`.
202
+ - **A way back to a custom value.** Template mode's chip editor draws no `×`
203
+ of its own, so a field that entered template mode from the ⋮ ("Use Template")
204
+ offered nothing to leave it with. The ⋮ now carries "Use Custom Value"
205
+ whenever the field is in template mode and accepts custom values; leaving
206
+ clears the value only when it holds a reference (which would put the field
207
+ straight back into template mode) and keeps anything the author typed.
208
+ - **A lone group in the ⋮ opens itself.** Two of the menu's groups are
209
+ collapsed sections (`MenuActionsSection`, `CustomMenuItems`), so a menu
210
+ holding nothing but one of them asked for a click to reach the only thing on
211
+ offer — on an untyped field, "Set Custom Value" hiding the data types behind
212
+ it. It now starts expanded when it is the menu's only group; it keeps its
213
+ label, so the rows still say what they are, and can still be collapsed. Where
214
+ the menu holds more than one group the sections stay shut: there the click is
215
+ the choice. The same rule the template list follows for a lone category, and
216
+ the row menu already publishes these rows flat. Guarded by
217
+ `__tests__/loneMenuSectionOpensItself.test.tsx` (the section's own contract)
218
+ and the story *Auto · Via Form Engine Menu Opens Its Only Group* (the rule, in
219
+ a real browser — the menu lives in a popover jsdom never opens).
220
+ - **No document toolbar on a form field.** `RichTextFormField` asked Reqore
221
+ for undo and redo but not styling, and Reqore draws that bar as a panel as
222
+ wide as the tag list: an empty 600px box under the field holding two greyed
223
+ icons — they are disabled until there is history to walk. It is off for every
224
+ form use now; both still work from the keyboard, where every other text
225
+ field's do. The `richtext` type (the "Text" row of the data-type submenu) is
226
+ where this was reported. Guarded by
227
+ `__tests__/richTextFieldHasNoToolbar.test.tsx`.
228
+
229
+ **Revised 2026-09-16 — a field says what it takes, and an untyped one is
230
+ checked as the type it was given.** Two rules from the same report (picking
231
+ "Binary" and being told nothing about the value):
232
+
233
+ - **A binary field names its encodings.** Its placeholder reads
234
+ `Base64 — or 0x-prefixed hex, or a data:…;base64 URL`, which is what the
235
+ server actually decodes: Qorus `lib/misc.ql`
236
+ (`_priv_parse_ui_hash_value_intern`) reads base64 unless the value starts
237
+ `0x` or `data:<mime>;base64,`, symmetric with the `toBase64()` it encodes
238
+ with (`Classes/UserApi.qc`). Bare hex is the trap — it is not rejected, it is
239
+ read as base64 and corrupts. `validateField`'s `binary` branch already
240
+ accepted exactly these three spellings; the placeholder is that rule said
241
+ before the fact instead of after. A caller's own placeholder still wins, and
242
+ `data` is left alone — it is not base64. Guarded by
243
+ `__tests__/binaryFieldSaysWhatItTakes.test.tsx`.
244
+ - **An untyped option is validated as the type its VALUE carries.** `auto` and
245
+ `any` declare no type: the author picks one and it is recorded beside the
246
+ value. `getOptionFieldMessages` read the schema first, so a binary value was
247
+ validated as `auto` — which auto-detects from the value and accepts whatever
248
+ is there — while the form's own check prefers the stored type
249
+ (`getOptionFieldStorageType`). The two disagreed, so the form said "a field
250
+ is not valid and requires attention" while the field it meant gave no reason.
251
+ The messages now use the same precedence; a schema that names a concrete type
252
+ still wins, because the schema is what the field accepts.
253
+ `isUntypedOptionType` (`helpers/optionUiTypes.ts`) is the shared spelling.
254
+ Guarded by `__tests__/untypedFieldReportsItsChosenType.test.ts`.
255
+
256
+ **Revised 2026-09-16 (audit pass) — three rules the review pulled out of the
257
+ above.**
258
+
259
+ - **Whatever the field's own ⋮ offers, its ROW menu offers too.** A field
260
+ inside a compact row draws no menu of its own and publishes into the row's
261
+ (`rowMenuContext`). "Use Custom Value" was added to the drawn menu only, so
262
+ on the surface most options are actually edited from, template mode was still
263
+ a one-way door. The published list now carries it on the same condition, with
264
+ its own token in `publishedKey` — that key is what gates re-publication, so a
265
+ row without one never re-publishes when template mode flips. Guarded by
266
+ `__tests__/rowMenuOffersTheWayOutOfTemplateMode.test.tsx`.
267
+ - **`RichTextFormField` takes `tags` and does not forward it.** The editor's
268
+ `tags` is its template catalogue; a form field's `tags` is the chips to show.
269
+ Two different things under one name, and `{...rest}` sat after the computed
270
+ value, so a field spreading its props erased the template list — patched at
271
+ two call sites with `omit(rest, 'tags')` and latent at two more. `rest` is now
272
+ spread FIRST, `tags` is declared as the field's chips and dropped, and the
273
+ patches are gone.
274
+ - **One list of the untyped spellings.** `any`/`auto` was hand-rolled in eight
275
+ places. `UNTYPED_OPTION_TYPES` / `isUntypedOptionType` in
276
+ `helpers/optionUiTypes.ts` is now the one, and `engine/typeChoices`
277
+ re-exports it as `BuiltInAnyLikeTypes` for its existing callers. It lives in
278
+ helpers because a helper cannot import from a component.
279
+
280
+ Note that a `FormEngine` option only offers templates when it declares
281
+ `supports_templates` — a field with no ⋮ template entry is that option's
282
+ schema speaking, not a defect.
283
+
132
284
  ### Out of scope for phase 1
133
285
 
134
286
  - The `type-depends-on` / `requestFieldData` cross-field type
@@ -178,7 +330,9 @@ qorus-ide coupling and copies over near-verbatim. The real work is the
178
330
  in `CatalogLeafForm`. `catalogLeafToFieldSchema` already emits the
179
331
  right shape; the adapter wires `value`/`onChange`. This is where the
180
332
  phase-1 `auto` field pays off — leaves typed `auto` now render a
181
- real type-picker instead of a textarea.
333
+ real type-picker instead of a textarea. (Revised 2026-09-14: an empty
334
+ `auto` leaf is a typable editor again, with the type on its ⋮ menu — see
335
+ phase 1.)
182
336
  3. Port `CatalogNodeEditor` (group/map/list dispatch + drawer editors),
183
337
  swapping qorus-ide's `QorusTable` for `ReqoreTable` (or a Reqraft
184
338
  table if one exists) and `Hint` for a Reqraft equivalent or nothing.
@@ -78,13 +78,79 @@ errors: -32602 missing/invalid expression; -32803 render failure;
78
78
  -32601 on servers predating the method
79
79
  ```
80
80
 
81
- ## Fallback behaviour (reqraft)
82
-
83
- `useRenderExpression` keeps the public signature `{ render, serverRendering }`:
84
-
85
- - Server-first; on LSP unreachable (bounded connection wait + cooldown) or
86
- `-32601` (older server remembered for the page lifetime), `render`
87
- falls back to the client-side approximation (`renderExpressionToText`)
88
- and `serverRendering` reports `false`.
89
- - Consumers (`builder/renderTemplate.tsx`, `ExpressionField.tsx`) are
90
- untouched.
81
+ ## Waiting, not approximating (reqraft)
82
+
83
+ `useRenderExpression()` returns `{ render, renderRich }`; both resolve the
84
+ server's rendering or `null`:
85
+
86
+ - A render **waits for the connection** there is no timer racing it. The
87
+ connection has its own reconnect schedule and rejects once it gives up
88
+ (see below), so a render never outlives a dead socket.
89
+ - `null` means the server cannot render: the connection gave up, or the server
90
+ predates the method (`-32601`, remembered for the page lifetime).
91
+ - There is **no client-side stand-in**. A Qorus form does not work without
92
+ Qorus, so the approximation only ever appeared while the page's first
93
+ connection was still opening, marked "Approximate" on the first Explain of
94
+ every page. `renderExpressionToText` survives only as the synchronous summary
95
+ a collapsed row prints (`readFirst.ts`), and is never shown in place of a
96
+ server rendering.
97
+
98
+ ## How an expression is shown when it is not being edited
99
+
100
+ Every read-only expression surface draws through **`DpqlRendering`**
101
+ (`src/components/form/expressions/DpqlRendering.tsx`): the Explain panel, the
102
+ Text view's Preview, the conversion a type-fit message suggests, and a collapsed
103
+ expression row. It is a read-only `DpqlEditor` with diagnostics and hover off,
104
+ so every one of them reads the same — monospace, coloured by the language
105
+ server's semantic tokens (the canonical tokenizer; there is no client-side
106
+ highlighter), template references as chips.
107
+
108
+ - `ExpressionRendering` renders an AST: it waits in the form's one waiting shape
109
+ (`FormFieldsSkeleton`, `data-wait="expression-rendering"`), then shows the
110
+ server's rendering, or says "This expression could not be rendered." when the
111
+ server cannot.
112
+ - The Preview appears only once the server's rendering exists and differs from
113
+ the typed text; it has no waiting state of its own.
114
+ - A surface with a template catalogue passes it (`templates`): a reference the
115
+ catalogue knows reads as the name it was chosen by, with the entry's
116
+ description as its hover — the Discord assistant's Save Reply row reads
117
+ `trim(Choices[0].message.content)`. One it does not know keeps its path
118
+ (`data: dc_ai_reply.choices[0].message.content`). A collapsed row passes its
119
+ field's own templates, else the form's; the builder's Explain passes its own.
120
+ - A reference's path takes every dot except one that starts a method call, so
121
+ the server's `$local:str.endsWith($local:p, true)` chips `$local:str`, not
122
+ `str.endsWith`.
123
+ - A new read-only expression surface uses `DpqlRendering`, never its own
124
+ `<code>`, rich-text field or chip splitter.
125
+
126
+ ## Lifetime of the shared render socket
127
+
128
+ The module-level render client never releases its connection: the first
129
+ expression rendered on a page holds the endpoint's `LspSharedConnection` for
130
+ the rest of the page. Two consequences are designed for:
131
+
132
+ - **A socket that gives up is forgotten.** When the socket exhausts its
133
+ reconnect attempts, the connection fails any handshake still waiting and
134
+ drops the socket; the next `connect()` from any client on that endpoint
135
+ dials afresh. Without this, a first handshake that never completed held
136
+ every later editor on "Connecting to language server…" for the life of the
137
+ page, and one that had completed kept reporting the dead socket as ready.
138
+ - **Storybook resets it per story.** Stories run in one page, so the
139
+ connection an earlier story opened would otherwise serve a later story —
140
+ including one that starts its own mock LSP, which is then never dialled.
141
+ `.storybook/preview.tsx` runs `_resetRenderExpressionTransportForTests()`
142
+ and `_resetSharedLspConnectionsForTests()` in a project-level `beforeEach`.
143
+ - **Every story has a language server.** Because renderings wait rather than
144
+ approximate, the same `beforeEach` starts the mock DPQL server
145
+ (`dpqlMockLsp.ts`) for every story that is not `live`. Its answers follow the
146
+ real server's (`dpqlMockLanguage.ts`, pinned in
147
+ `__tests__/dpqlMockLanguage.test.ts`); a story that needs other answers starts
148
+ its own server on the URL, which replaces the default.
149
+ - **The rendering is spelled by the catalogue.** The server renders a function
150
+ by name (`concat("a", $local:x)`), an expression with a `render_template` by
151
+ that template (`$arg[0].startsWith($arg[1], $arg[2])`, a missing argument
152
+ read as its `default_value`), and any other operator as its arguments joined
153
+ by its symbol. A two-argument word comparison serializes infix
154
+ (`"test" startsWith "t"`) and otherwise as a call by symbol. The story server
155
+ reads `mockExpressions` by the same rules, which is why that fixture carries
156
+ the live `render_template`s and groups.
@@ -253,6 +253,27 @@ Concrete plumbing:
253
253
  `SELECT * FROM users WHERE name = "Alice"` is replaced with a
254
254
  valid DPQL expression (e.g. `1 == 1` or `2026 - 2024 == 2`).
255
255
 
256
+ **The server holds the text the editor shows** *(revised 2026-09-14)*. The
257
+ server colours and diagnoses its own copy of the document, so two things must
258
+ stay true, and neither did for a value set from outside rather than typed:
259
+
260
+ - **Every value reaches the server.** `SmartEditor` passes each `value` to
261
+ `session.didChange`, which sends only text the server does not already hold,
262
+ and holds text set before the document opened for `didOpen`. Before, only
263
+ typing was sent and `didOpen` carried the text at mount, so a Preview that
264
+ follows the author was coloured for text it no longer showed.
265
+ - **Offsets use the tree the editor holds.** Slate normalises a value set from
266
+ outside — a leading chip gains an empty text leaf before it — without
267
+ changing its text, so `value` does not change. `SmartEditor` adopts every
268
+ tree the editor reports as the one token, diagnostic and signature offsets
269
+ are computed against. Before, it kept the pre-normalisation tree and every
270
+ offset pointed one leaf off: a Text view seeded with
271
+ `"$local:name" == "John"` was never highlighted.
272
+
273
+ Guarded by `__tests__/smartEditor/sessionKeepsServerText.test.ts` and the
274
+ `ExpressionField` story *Via Form Engine Text Mode*, which asserts the seeded
275
+ text is coloured.
276
+
256
277
  **Surface area.**
257
278
  `src/utils/lspClient.ts` (legend capture, ~10 lines),
258
279
  `src/utils/lspClient.types.ts` (new `ILspSemanticTokensLegend` /
@@ -1 +1 @@
1
- {"version":3,"file":"DpqlEditor.d.ts","sourceRoot":"","sources":["../../../src/components/dpqlEditor/DpqlEditor.tsx"],"names":[],"mappings":";AA2BA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG3D,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC;AASjD,eAAO,MAAM,UAAU,6GA4KtB,CAAC"}
1
+ {"version":3,"file":"DpqlEditor.d.ts","sourceRoot":"","sources":["../../../src/components/dpqlEditor/DpqlEditor.tsx"],"names":[],"mappings":";AA2BA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG3D,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC;AASjD,eAAO,MAAM,UAAU,6GA+KtB,CAAC"}
@@ -75,7 +75,7 @@ var useDpqlSession_1 = require("./useDpqlSession");
75
75
  // of completion is handled by typing inside an already-open token.
76
76
  var DPQL_TRIGGERS = new Set(['@', '$', '.', ':']);
77
77
  exports.DpqlEditor = (0, react_1.forwardRef)(function (_a, ref) {
78
- var value = _a.value, onChange = _a.onChange, provider = _a.provider, recordType = _a.recordType, options = _a.options, actionCode = _a.actionCode, height = _a.height, _b = _a.readOnly, readOnly = _b === void 0 ? false : _b, _c = _a.showDiagnostics, showDiagnostics = _c === void 0 ? true : _c, _d = _a.enableHover, enableHover = _d === void 0 ? true : _d, _e = _a.templateTagsUseIntent, templateTagsUseIntent = _e === void 0 ? false : _e, onBlur = _a.onBlur, _f = _a.useServerParse, useServerParse = _f === void 0 ? false : _f, _g = _a.alertPayloadContext, alertPayloadContext = _g === void 0 ? false : _g, fsmContext = _a.fsmContext;
78
+ var value = _a.value, onChange = _a.onChange, provider = _a.provider, recordType = _a.recordType, options = _a.options, actionCode = _a.actionCode, height = _a.height, _b = _a.readOnly, readOnly = _b === void 0 ? false : _b, _c = _a.showDiagnostics, showDiagnostics = _c === void 0 ? true : _c, _d = _a.enableHover, enableHover = _d === void 0 ? true : _d, _e = _a.templateTagsUseIntent, templateTagsUseIntent = _e === void 0 ? false : _e, loadingIndicator = _a.loadingIndicator, templates = _a.templates, onBlur = _a.onBlur, _f = _a.useServerParse, useServerParse = _f === void 0 ? false : _f, _g = _a.alertPayloadContext, alertPayloadContext = _g === void 0 ? false : _g, fsmContext = _a.fsmContext;
79
79
  var dpql = (0, useDpqlSession_1.useDpqlSession)({
80
80
  provider: provider,
81
81
  recordType: recordType,
@@ -180,7 +180,7 @@ exports.DpqlEditor = (0, react_1.forwardRef)(function (_a, ref) {
180
180
  return dpqlHelpers_1.dpqlSlateConverter.toSlateNodes(text);
181
181
  } });
182
182
  }, [useServerParse, serverParsedFor, serverParsedNodes]);
183
- var tagRenderer = (0, react_1.useMemo)(function () { return (0, dpqlTags_1.makeDpqlTagRenderer)(dpql.fieldMeta, { templateTagsUseIntent: templateTagsUseIntent }); }, [dpql.fieldMeta, templateTagsUseIntent]);
183
+ var tagRenderer = (0, react_1.useMemo)(function () { return (0, dpqlTags_1.makeDpqlTagRenderer)(dpql.fieldMeta, { templateTagsUseIntent: templateTagsUseIntent, templates: templates }); }, [dpql.fieldMeta, templateTagsUseIntent, templates]);
184
184
  (0, react_1.useImperativeHandle)(ref, function () { return ({
185
185
  format: function () { return __awaiter(void 0, void 0, void 0, function () {
186
186
  var formatted;
@@ -200,7 +200,7 @@ exports.DpqlEditor = (0, react_1.forwardRef)(function (_a, ref) {
200
200
  parse: dpql.parse,
201
201
  serialize: dpql.serialize,
202
202
  }); }, [dpql, value, onChange]);
203
- return ((0, jsx_runtime_1.jsx)(SmartEditor_1.SmartEditor, { session: dpql.session, value: value, onChange: onChange, tagRenderer: tagRenderer, triggerCharacters: DPQL_TRIGGERS, converter: converter, completionInserter: dpqlInserter_1.dpqlCompletionInserter, height: height, readOnly: readOnly, showDiagnostics: showDiagnostics, enableHover: enableHover, onBlur: onBlur, isLoading: isParsing }));
203
+ return ((0, jsx_runtime_1.jsx)(SmartEditor_1.SmartEditor, { session: dpql.session, value: value, onChange: onChange, tagRenderer: tagRenderer, triggerCharacters: DPQL_TRIGGERS, converter: converter, completionInserter: dpqlInserter_1.dpqlCompletionInserter, height: height, readOnly: readOnly, showDiagnostics: showDiagnostics, enableHover: enableHover, onBlur: onBlur, isLoading: isParsing, loadingIndicator: loadingIndicator }));
204
204
  });
205
205
  exports.DpqlEditor.displayName = 'DpqlEditor';
206
206
  //# sourceMappingURL=DpqlEditor.js.map