@proposit/proposit-core 0.8.9

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 (429) hide show
  1. package/README.md +1032 -0
  2. package/dist/cli/commands/analysis.d.ts +3 -0
  3. package/dist/cli/commands/analysis.d.ts.map +1 -0
  4. package/dist/cli/commands/analysis.js +504 -0
  5. package/dist/cli/commands/analysis.js.map +1 -0
  6. package/dist/cli/commands/arguments.d.ts +3 -0
  7. package/dist/cli/commands/arguments.d.ts.map +1 -0
  8. package/dist/cli/commands/arguments.js +187 -0
  9. package/dist/cli/commands/arguments.js.map +1 -0
  10. package/dist/cli/commands/claims.d.ts +3 -0
  11. package/dist/cli/commands/claims.d.ts.map +1 -0
  12. package/dist/cli/commands/claims.js +120 -0
  13. package/dist/cli/commands/claims.js.map +1 -0
  14. package/dist/cli/commands/diff.d.ts +3 -0
  15. package/dist/cli/commands/diff.d.ts.map +1 -0
  16. package/dist/cli/commands/diff.js +61 -0
  17. package/dist/cli/commands/diff.js.map +1 -0
  18. package/dist/cli/commands/expressions.d.ts +3 -0
  19. package/dist/cli/commands/expressions.d.ts.map +1 -0
  20. package/dist/cli/commands/expressions.js +344 -0
  21. package/dist/cli/commands/expressions.js.map +1 -0
  22. package/dist/cli/commands/graph.d.ts +13 -0
  23. package/dist/cli/commands/graph.d.ts.map +1 -0
  24. package/dist/cli/commands/graph.js +382 -0
  25. package/dist/cli/commands/graph.js.map +1 -0
  26. package/dist/cli/commands/meta.d.ts +3 -0
  27. package/dist/cli/commands/meta.d.ts.map +1 -0
  28. package/dist/cli/commands/meta.js +14 -0
  29. package/dist/cli/commands/meta.js.map +1 -0
  30. package/dist/cli/commands/parse.d.ts +3 -0
  31. package/dist/cli/commands/parse.d.ts.map +1 -0
  32. package/dist/cli/commands/parse.js +171 -0
  33. package/dist/cli/commands/parse.js.map +1 -0
  34. package/dist/cli/commands/premises.d.ts +3 -0
  35. package/dist/cli/commands/premises.d.ts.map +1 -0
  36. package/dist/cli/commands/premises.js +261 -0
  37. package/dist/cli/commands/premises.js.map +1 -0
  38. package/dist/cli/commands/render.d.ts +3 -0
  39. package/dist/cli/commands/render.d.ts.map +1 -0
  40. package/dist/cli/commands/render.js +109 -0
  41. package/dist/cli/commands/render.js.map +1 -0
  42. package/dist/cli/commands/repair.d.ts +3 -0
  43. package/dist/cli/commands/repair.d.ts.map +1 -0
  44. package/dist/cli/commands/repair.js +53 -0
  45. package/dist/cli/commands/repair.js.map +1 -0
  46. package/dist/cli/commands/roles.d.ts +3 -0
  47. package/dist/cli/commands/roles.d.ts.map +1 -0
  48. package/dist/cli/commands/roles.js +64 -0
  49. package/dist/cli/commands/roles.js.map +1 -0
  50. package/dist/cli/commands/sources.d.ts +3 -0
  51. package/dist/cli/commands/sources.d.ts.map +1 -0
  52. package/dist/cli/commands/sources.js +103 -0
  53. package/dist/cli/commands/sources.js.map +1 -0
  54. package/dist/cli/commands/validate.d.ts +3 -0
  55. package/dist/cli/commands/validate.d.ts.map +1 -0
  56. package/dist/cli/commands/validate.js +27 -0
  57. package/dist/cli/commands/validate.js.map +1 -0
  58. package/dist/cli/commands/variables.d.ts +3 -0
  59. package/dist/cli/commands/variables.d.ts.map +1 -0
  60. package/dist/cli/commands/variables.js +206 -0
  61. package/dist/cli/commands/variables.js.map +1 -0
  62. package/dist/cli/commands/version-show.d.ts +3 -0
  63. package/dist/cli/commands/version-show.d.ts.map +1 -0
  64. package/dist/cli/commands/version-show.js +31 -0
  65. package/dist/cli/commands/version-show.js.map +1 -0
  66. package/dist/cli/config.d.ts +8 -0
  67. package/dist/cli/config.d.ts.map +1 -0
  68. package/dist/cli/config.js +24 -0
  69. package/dist/cli/config.js.map +1 -0
  70. package/dist/cli/engine.d.ts +19 -0
  71. package/dist/cli/engine.d.ts.map +1 -0
  72. package/dist/cli/engine.js +173 -0
  73. package/dist/cli/engine.js.map +1 -0
  74. package/dist/cli/import.d.ts +22 -0
  75. package/dist/cli/import.d.ts.map +1 -0
  76. package/dist/cli/import.js +242 -0
  77. package/dist/cli/import.js.map +1 -0
  78. package/dist/cli/llm/index.d.ts +6 -0
  79. package/dist/cli/llm/index.d.ts.map +1 -0
  80. package/dist/cli/llm/index.js +26 -0
  81. package/dist/cli/llm/index.js.map +1 -0
  82. package/dist/cli/llm/openai.d.ts +4 -0
  83. package/dist/cli/llm/openai.d.ts.map +1 -0
  84. package/dist/cli/llm/openai.js +44 -0
  85. package/dist/cli/llm/openai.js.map +1 -0
  86. package/dist/cli/llm/types.d.ts +14 -0
  87. package/dist/cli/llm/types.d.ts.map +1 -0
  88. package/dist/cli/llm/types.js +2 -0
  89. package/dist/cli/llm/types.js.map +1 -0
  90. package/dist/cli/logging.d.ts +8 -0
  91. package/dist/cli/logging.d.ts.map +1 -0
  92. package/dist/cli/logging.js +23 -0
  93. package/dist/cli/logging.js.map +1 -0
  94. package/dist/cli/output/diff-renderer.d.ts +4 -0
  95. package/dist/cli/output/diff-renderer.d.ts.map +1 -0
  96. package/dist/cli/output/diff-renderer.js +89 -0
  97. package/dist/cli/output/diff-renderer.js.map +1 -0
  98. package/dist/cli/output.d.ts +6 -0
  99. package/dist/cli/output.d.ts.map +1 -0
  100. package/dist/cli/output.js +41 -0
  101. package/dist/cli/output.js.map +1 -0
  102. package/dist/cli/router.d.ts +14 -0
  103. package/dist/cli/router.d.ts.map +1 -0
  104. package/dist/cli/router.js +59 -0
  105. package/dist/cli/router.js.map +1 -0
  106. package/dist/cli/schemata.d.ts +74 -0
  107. package/dist/cli/schemata.d.ts.map +1 -0
  108. package/dist/cli/schemata.js +89 -0
  109. package/dist/cli/schemata.js.map +1 -0
  110. package/dist/cli/storage/analysis.d.ts +9 -0
  111. package/dist/cli/storage/analysis.d.ts.map +1 -0
  112. package/dist/cli/storage/analysis.js +108 -0
  113. package/dist/cli/storage/analysis.js.map +1 -0
  114. package/dist/cli/storage/arguments.d.ts +12 -0
  115. package/dist/cli/storage/arguments.d.ts.map +1 -0
  116. package/dist/cli/storage/arguments.js +80 -0
  117. package/dist/cli/storage/arguments.js.map +1 -0
  118. package/dist/cli/storage/libraries.d.ts +14 -0
  119. package/dist/cli/storage/libraries.d.ts.map +1 -0
  120. package/dist/cli/storage/libraries.js +80 -0
  121. package/dist/cli/storage/libraries.js.map +1 -0
  122. package/dist/cli/storage/premises.d.ts +9 -0
  123. package/dist/cli/storage/premises.d.ts.map +1 -0
  124. package/dist/cli/storage/premises.js +67 -0
  125. package/dist/cli/storage/premises.js.map +1 -0
  126. package/dist/cli/storage/roles.d.ts +4 -0
  127. package/dist/cli/storage/roles.d.ts.map +1 -0
  128. package/dist/cli/storage/roles.js +26 -0
  129. package/dist/cli/storage/roles.js.map +1 -0
  130. package/dist/cli/storage/variables.d.ts +4 -0
  131. package/dist/cli/storage/variables.d.ts.map +1 -0
  132. package/dist/cli/storage/variables.js +36 -0
  133. package/dist/cli/storage/variables.js.map +1 -0
  134. package/dist/cli.d.ts +3 -0
  135. package/dist/cli.d.ts.map +1 -0
  136. package/dist/cli.js +65 -0
  137. package/dist/cli.js.map +1 -0
  138. package/dist/extensions/basics/argument-parser.d.ts +12 -0
  139. package/dist/extensions/basics/argument-parser.d.ts.map +1 -0
  140. package/dist/extensions/basics/argument-parser.js +27 -0
  141. package/dist/extensions/basics/argument-parser.js.map +1 -0
  142. package/dist/extensions/basics/index.d.ts +4 -0
  143. package/dist/extensions/basics/index.d.ts.map +1 -0
  144. package/dist/extensions/basics/index.js +3 -0
  145. package/dist/extensions/basics/index.js.map +1 -0
  146. package/dist/extensions/basics/schemata.d.ts +35 -0
  147. package/dist/extensions/basics/schemata.d.ts.map +1 -0
  148. package/dist/extensions/basics/schemata.js +55 -0
  149. package/dist/extensions/basics/schemata.js.map +1 -0
  150. package/dist/extensions/ieee/formatting.d.ts +18 -0
  151. package/dist/extensions/ieee/formatting.d.ts.map +1 -0
  152. package/dist/extensions/ieee/formatting.js +57 -0
  153. package/dist/extensions/ieee/formatting.js.map +1 -0
  154. package/dist/extensions/ieee/index.d.ts +6 -0
  155. package/dist/extensions/ieee/index.d.ts.map +1 -0
  156. package/dist/extensions/ieee/index.js +6 -0
  157. package/dist/extensions/ieee/index.js.map +1 -0
  158. package/dist/extensions/ieee/references.d.ts +1379 -0
  159. package/dist/extensions/ieee/references.d.ts.map +1 -0
  160. package/dist/extensions/ieee/references.js +929 -0
  161. package/dist/extensions/ieee/references.js.map +1 -0
  162. package/dist/extensions/ieee/relaxed.d.ts +1371 -0
  163. package/dist/extensions/ieee/relaxed.d.ts.map +1 -0
  164. package/dist/extensions/ieee/relaxed.js +160 -0
  165. package/dist/extensions/ieee/relaxed.js.map +1 -0
  166. package/dist/extensions/ieee/segment-builder.d.ts +9 -0
  167. package/dist/extensions/ieee/segment-builder.d.ts.map +1 -0
  168. package/dist/extensions/ieee/segment-builder.js +98 -0
  169. package/dist/extensions/ieee/segment-builder.js.map +1 -0
  170. package/dist/extensions/ieee/segment-templates.d.ts +58 -0
  171. package/dist/extensions/ieee/segment-templates.d.ts.map +1 -0
  172. package/dist/extensions/ieee/segment-templates.js +1618 -0
  173. package/dist/extensions/ieee/segment-templates.js.map +1 -0
  174. package/dist/extensions/ieee/source.d.ts +434 -0
  175. package/dist/extensions/ieee/source.d.ts.map +1 -0
  176. package/dist/extensions/ieee/source.js +12 -0
  177. package/dist/extensions/ieee/source.js.map +1 -0
  178. package/dist/index.d.ts +10 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +10 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/lib/consts.d.ts +21 -0
  183. package/dist/lib/consts.d.ts.map +1 -0
  184. package/dist/lib/consts.js +117 -0
  185. package/dist/lib/consts.js.map +1 -0
  186. package/dist/lib/core/argument-engine.d.ts +181 -0
  187. package/dist/lib/core/argument-engine.d.ts.map +1 -0
  188. package/dist/lib/core/argument-engine.js +1294 -0
  189. package/dist/lib/core/argument-engine.js.map +1 -0
  190. package/dist/lib/core/argument-library.d.ts +84 -0
  191. package/dist/lib/core/argument-library.d.ts.map +1 -0
  192. package/dist/lib/core/argument-library.js +122 -0
  193. package/dist/lib/core/argument-library.js.map +1 -0
  194. package/dist/lib/core/argument-validation.d.ts +74 -0
  195. package/dist/lib/core/argument-validation.d.ts.map +1 -0
  196. package/dist/lib/core/argument-validation.js +315 -0
  197. package/dist/lib/core/argument-validation.js.map +1 -0
  198. package/dist/lib/core/change-collector.d.ts +24 -0
  199. package/dist/lib/core/change-collector.d.ts.map +1 -0
  200. package/dist/lib/core/change-collector.js +63 -0
  201. package/dist/lib/core/change-collector.js.map +1 -0
  202. package/dist/lib/core/checksum.d.ts +15 -0
  203. package/dist/lib/core/checksum.d.ts.map +1 -0
  204. package/dist/lib/core/checksum.js +43 -0
  205. package/dist/lib/core/checksum.js.map +1 -0
  206. package/dist/lib/core/claim-library.d.ts +23 -0
  207. package/dist/lib/core/claim-library.d.ts.map +1 -0
  208. package/dist/lib/core/claim-library.js +21 -0
  209. package/dist/lib/core/claim-library.js.map +1 -0
  210. package/dist/lib/core/claim-source-library.d.ts +32 -0
  211. package/dist/lib/core/claim-source-library.d.ts.map +1 -0
  212. package/dist/lib/core/claim-source-library.js +193 -0
  213. package/dist/lib/core/claim-source-library.js.map +1 -0
  214. package/dist/lib/core/diff.d.ts +20 -0
  215. package/dist/lib/core/diff.d.ts.map +1 -0
  216. package/dist/lib/core/diff.js +260 -0
  217. package/dist/lib/core/diff.js.map +1 -0
  218. package/dist/lib/core/evaluation/argument-evaluation.d.ts +53 -0
  219. package/dist/lib/core/evaluation/argument-evaluation.d.ts.map +1 -0
  220. package/dist/lib/core/evaluation/argument-evaluation.js +535 -0
  221. package/dist/lib/core/evaluation/argument-evaluation.js.map +1 -0
  222. package/dist/lib/core/evaluation/grading.d.ts +28 -0
  223. package/dist/lib/core/evaluation/grading.d.ts.map +1 -0
  224. package/dist/lib/core/evaluation/grading.js +44 -0
  225. package/dist/lib/core/evaluation/grading.js.map +1 -0
  226. package/dist/lib/core/evaluation/kleene.d.ts +12 -0
  227. package/dist/lib/core/evaluation/kleene.d.ts.map +1 -0
  228. package/dist/lib/core/evaluation/kleene.js +29 -0
  229. package/dist/lib/core/evaluation/kleene.js.map +1 -0
  230. package/dist/lib/core/evaluation/validation.d.ts +10 -0
  231. package/dist/lib/core/evaluation/validation.d.ts.map +1 -0
  232. package/dist/lib/core/evaluation/validation.js +28 -0
  233. package/dist/lib/core/evaluation/validation.js.map +1 -0
  234. package/dist/lib/core/expression-manager.d.ts +278 -0
  235. package/dist/lib/core/expression-manager.d.ts.map +1 -0
  236. package/dist/lib/core/expression-manager.js +1622 -0
  237. package/dist/lib/core/expression-manager.js.map +1 -0
  238. package/dist/lib/core/fork-library.d.ts +26 -0
  239. package/dist/lib/core/fork-library.d.ts.map +1 -0
  240. package/dist/lib/core/fork-library.js +71 -0
  241. package/dist/lib/core/fork-library.js.map +1 -0
  242. package/dist/lib/core/fork-namespace.d.ts +32 -0
  243. package/dist/lib/core/fork-namespace.d.ts.map +1 -0
  244. package/dist/lib/core/fork-namespace.js +99 -0
  245. package/dist/lib/core/fork-namespace.js.map +1 -0
  246. package/dist/lib/core/fork.d.ts +30 -0
  247. package/dist/lib/core/fork.d.ts.map +1 -0
  248. package/dist/lib/core/fork.js +125 -0
  249. package/dist/lib/core/fork.js.map +1 -0
  250. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +366 -0
  251. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -0
  252. package/dist/lib/core/interfaces/argument-engine.interfaces.js +2 -0
  253. package/dist/lib/core/interfaces/argument-engine.interfaces.js.map +1 -0
  254. package/dist/lib/core/interfaces/index.d.ts +5 -0
  255. package/dist/lib/core/interfaces/index.d.ts.map +1 -0
  256. package/dist/lib/core/interfaces/index.js +2 -0
  257. package/dist/lib/core/interfaces/index.js.map +1 -0
  258. package/dist/lib/core/interfaces/library.interfaces.d.ts +347 -0
  259. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -0
  260. package/dist/lib/core/interfaces/library.interfaces.js +2 -0
  261. package/dist/lib/core/interfaces/library.interfaces.js.map +1 -0
  262. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +401 -0
  263. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -0
  264. package/dist/lib/core/interfaces/premise-engine.interfaces.js +2 -0
  265. package/dist/lib/core/interfaces/premise-engine.interfaces.js.map +1 -0
  266. package/dist/lib/core/interfaces/shared.interfaces.d.ts +28 -0
  267. package/dist/lib/core/interfaces/shared.interfaces.d.ts.map +1 -0
  268. package/dist/lib/core/interfaces/shared.interfaces.js +2 -0
  269. package/dist/lib/core/interfaces/shared.interfaces.js.map +1 -0
  270. package/dist/lib/core/invariant-violation-error.d.ts +10 -0
  271. package/dist/lib/core/invariant-violation-error.d.ts.map +1 -0
  272. package/dist/lib/core/invariant-violation-error.js +16 -0
  273. package/dist/lib/core/invariant-violation-error.js.map +1 -0
  274. package/dist/lib/core/parser/formula-gen.js +923 -0
  275. package/dist/lib/core/parser/formula.d.ts +24 -0
  276. package/dist/lib/core/parser/formula.d.ts.map +1 -0
  277. package/dist/lib/core/parser/formula.js +8 -0
  278. package/dist/lib/core/parser/formula.js.map +1 -0
  279. package/dist/lib/core/premise-engine.d.ts +122 -0
  280. package/dist/lib/core/premise-engine.d.ts.map +1 -0
  281. package/dist/lib/core/premise-engine.js +1362 -0
  282. package/dist/lib/core/premise-engine.js.map +1 -0
  283. package/dist/lib/core/proposit-core.d.ts +111 -0
  284. package/dist/lib/core/proposit-core.d.ts.map +1 -0
  285. package/dist/lib/core/proposit-core.js +365 -0
  286. package/dist/lib/core/proposit-core.js.map +1 -0
  287. package/dist/lib/core/relationships.d.ts +15 -0
  288. package/dist/lib/core/relationships.d.ts.map +1 -0
  289. package/dist/lib/core/relationships.js +319 -0
  290. package/dist/lib/core/relationships.js.map +1 -0
  291. package/dist/lib/core/source-library.d.ts +23 -0
  292. package/dist/lib/core/source-library.d.ts.map +1 -0
  293. package/dist/lib/core/source-library.js +21 -0
  294. package/dist/lib/core/source-library.js.map +1 -0
  295. package/dist/lib/core/variable-manager.d.ts +68 -0
  296. package/dist/lib/core/variable-manager.d.ts.map +1 -0
  297. package/dist/lib/core/variable-manager.js +200 -0
  298. package/dist/lib/core/variable-manager.js.map +1 -0
  299. package/dist/lib/core/versioned-library.d.ts +52 -0
  300. package/dist/lib/core/versioned-library.d.ts.map +1 -0
  301. package/dist/lib/core/versioned-library.js +192 -0
  302. package/dist/lib/core/versioned-library.js.map +1 -0
  303. package/dist/lib/index.d.ts +54 -0
  304. package/dist/lib/index.d.ts.map +1 -0
  305. package/dist/lib/index.js +39 -0
  306. package/dist/lib/index.js.map +1 -0
  307. package/dist/lib/parsing/argument-parser.d.ts +49 -0
  308. package/dist/lib/parsing/argument-parser.d.ts.map +1 -0
  309. package/dist/lib/parsing/argument-parser.js +403 -0
  310. package/dist/lib/parsing/argument-parser.js.map +1 -0
  311. package/dist/lib/parsing/clamp-max-lengths.d.ts +11 -0
  312. package/dist/lib/parsing/clamp-max-lengths.d.ts.map +1 -0
  313. package/dist/lib/parsing/clamp-max-lengths.js +56 -0
  314. package/dist/lib/parsing/clamp-max-lengths.js.map +1 -0
  315. package/dist/lib/parsing/index.d.ts +7 -0
  316. package/dist/lib/parsing/index.d.ts.map +1 -0
  317. package/dist/lib/parsing/index.js +4 -0
  318. package/dist/lib/parsing/index.js.map +1 -0
  319. package/dist/lib/parsing/prompt-builder.d.ts +11 -0
  320. package/dist/lib/parsing/prompt-builder.d.ts.map +1 -0
  321. package/dist/lib/parsing/prompt-builder.js +229 -0
  322. package/dist/lib/parsing/prompt-builder.js.map +1 -0
  323. package/dist/lib/parsing/schemata.d.ts +79 -0
  324. package/dist/lib/parsing/schemata.d.ts.map +1 -0
  325. package/dist/lib/parsing/schemata.js +92 -0
  326. package/dist/lib/parsing/schemata.js.map +1 -0
  327. package/dist/lib/parsing/types.d.ts +24 -0
  328. package/dist/lib/parsing/types.d.ts.map +1 -0
  329. package/dist/lib/parsing/types.js +2 -0
  330. package/dist/lib/parsing/types.js.map +1 -0
  331. package/dist/lib/schemata/analysis.d.ts +9 -0
  332. package/dist/lib/schemata/analysis.d.ts.map +1 -0
  333. package/dist/lib/schemata/analysis.js +13 -0
  334. package/dist/lib/schemata/analysis.js.map +1 -0
  335. package/dist/lib/schemata/argument.d.ts +14 -0
  336. package/dist/lib/schemata/argument.d.ts.map +1 -0
  337. package/dist/lib/schemata/argument.js +24 -0
  338. package/dist/lib/schemata/argument.js.map +1 -0
  339. package/dist/lib/schemata/claim.d.ts +9 -0
  340. package/dist/lib/schemata/claim.d.ts.map +1 -0
  341. package/dist/lib/schemata/claim.js +18 -0
  342. package/dist/lib/schemata/claim.js.map +1 -0
  343. package/dist/lib/schemata/fork.d.ts +76 -0
  344. package/dist/lib/schemata/fork.d.ts.map +1 -0
  345. package/dist/lib/schemata/fork.js +55 -0
  346. package/dist/lib/schemata/fork.js.map +1 -0
  347. package/dist/lib/schemata/import.d.ts +33 -0
  348. package/dist/lib/schemata/import.d.ts.map +1 -0
  349. package/dist/lib/schemata/import.js +18 -0
  350. package/dist/lib/schemata/import.js.map +1 -0
  351. package/dist/lib/schemata/index.d.ts +8 -0
  352. package/dist/lib/schemata/index.d.ts.map +1 -0
  353. package/dist/lib/schemata/index.js +8 -0
  354. package/dist/lib/schemata/index.js.map +1 -0
  355. package/dist/lib/schemata/propositional.d.ts +142 -0
  356. package/dist/lib/schemata/propositional.d.ts.map +1 -0
  357. package/dist/lib/schemata/propositional.js +120 -0
  358. package/dist/lib/schemata/propositional.js.map +1 -0
  359. package/dist/lib/schemata/shared.d.ts +41 -0
  360. package/dist/lib/schemata/shared.d.ts.map +1 -0
  361. package/dist/lib/schemata/shared.js +66 -0
  362. package/dist/lib/schemata/shared.js.map +1 -0
  363. package/dist/lib/schemata/source.d.ts +18 -0
  364. package/dist/lib/schemata/source.d.ts.map +1 -0
  365. package/dist/lib/schemata/source.js +35 -0
  366. package/dist/lib/schemata/source.js.map +1 -0
  367. package/dist/lib/types/checksum.d.ts +20 -0
  368. package/dist/lib/types/checksum.d.ts.map +1 -0
  369. package/dist/lib/types/checksum.js +2 -0
  370. package/dist/lib/types/checksum.js.map +1 -0
  371. package/dist/lib/types/diff.d.ts +60 -0
  372. package/dist/lib/types/diff.d.ts.map +1 -0
  373. package/dist/lib/types/diff.js +2 -0
  374. package/dist/lib/types/diff.js.map +1 -0
  375. package/dist/lib/types/evaluation.d.ts +164 -0
  376. package/dist/lib/types/evaluation.d.ts.map +1 -0
  377. package/dist/lib/types/evaluation.js +2 -0
  378. package/dist/lib/types/evaluation.js.map +1 -0
  379. package/dist/lib/types/fork.d.ts +25 -0
  380. package/dist/lib/types/fork.d.ts.map +1 -0
  381. package/dist/lib/types/fork.js +2 -0
  382. package/dist/lib/types/fork.js.map +1 -0
  383. package/dist/lib/types/grammar.d.ts +38 -0
  384. package/dist/lib/types/grammar.d.ts.map +1 -0
  385. package/dist/lib/types/grammar.js +11 -0
  386. package/dist/lib/types/grammar.js.map +1 -0
  387. package/dist/lib/types/mutation.d.ts +31 -0
  388. package/dist/lib/types/mutation.d.ts.map +1 -0
  389. package/dist/lib/types/mutation.js +2 -0
  390. package/dist/lib/types/mutation.js.map +1 -0
  391. package/dist/lib/types/reactive.d.ts +14 -0
  392. package/dist/lib/types/reactive.d.ts.map +1 -0
  393. package/dist/lib/types/reactive.js +2 -0
  394. package/dist/lib/types/reactive.js.map +1 -0
  395. package/dist/lib/types/relationships.d.ts +36 -0
  396. package/dist/lib/types/relationships.d.ts.map +1 -0
  397. package/dist/lib/types/relationships.js +2 -0
  398. package/dist/lib/types/relationships.js.map +1 -0
  399. package/dist/lib/types/validation.d.ts +47 -0
  400. package/dist/lib/types/validation.d.ts.map +1 -0
  401. package/dist/lib/types/validation.js +43 -0
  402. package/dist/lib/types/validation.js.map +1 -0
  403. package/dist/lib/utils/changeset.d.ts +124 -0
  404. package/dist/lib/utils/changeset.d.ts.map +1 -0
  405. package/dist/lib/utils/changeset.js +221 -0
  406. package/dist/lib/utils/changeset.js.map +1 -0
  407. package/dist/lib/utils/collections.d.ts +12 -0
  408. package/dist/lib/utils/collections.d.ts.map +1 -0
  409. package/dist/lib/utils/collections.js +24 -0
  410. package/dist/lib/utils/collections.js.map +1 -0
  411. package/dist/lib/utils/default-map.d.ts +17 -0
  412. package/dist/lib/utils/default-map.d.ts.map +1 -0
  413. package/dist/lib/utils/default-map.js +33 -0
  414. package/dist/lib/utils/default-map.js.map +1 -0
  415. package/dist/lib/utils/lookup.d.ts +47 -0
  416. package/dist/lib/utils/lookup.d.ts.map +1 -0
  417. package/dist/lib/utils/lookup.js +62 -0
  418. package/dist/lib/utils/lookup.js.map +1 -0
  419. package/dist/lib/utils/position.d.ts +12 -0
  420. package/dist/lib/utils/position.d.ts.map +1 -0
  421. package/dist/lib/utils/position.js +13 -0
  422. package/dist/lib/utils/position.js.map +1 -0
  423. package/package.json +82 -0
  424. package/skills/proposit-core/SKILL.md +35 -0
  425. package/skills/proposit-core/docs/api-usage.md +442 -0
  426. package/skills/proposit-core/docs/architecture.md +256 -0
  427. package/skills/proposit-core/docs/cli.md +304 -0
  428. package/skills/proposit-core/docs/testing.md +281 -0
  429. package/skills/proposit-core/docs/types-schemas.md +648 -0
@@ -0,0 +1,1622 @@
1
+ import { CorePropositionalExpressionSchema } from "../schemata/index.js";
2
+ import { getOrCreate } from "../utils/collections.js";
3
+ import { DEFAULT_POSITION_CONFIG, midpoint, } from "../utils/position.js";
4
+ import { defaultGenerateId } from "./argument-engine.js";
5
+ import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
6
+ import { entityChecksum, computeHash, canonicalSerialize } from "./checksum.js";
7
+ import { DEFAULT_GRAMMAR_CONFIG, } from "../types/grammar.js";
8
+ import { Value } from "typebox/value";
9
+ import { EXPR_SCHEMA_INVALID, EXPR_DUPLICATE_ID, EXPR_SELF_REFERENTIAL_PARENT, EXPR_PARENT_NOT_FOUND, EXPR_PARENT_NOT_CONTAINER, EXPR_ROOT_ONLY_VIOLATED, EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED, EXPR_CHILD_LIMIT_EXCEEDED, EXPR_POSITION_DUPLICATE, EXPR_CHECKSUM_MISMATCH, } from "../types/validation.js";
10
+ const PERMITTED_OPERATOR_SWAPS = {
11
+ and: "or",
12
+ or: "and",
13
+ implies: "iff",
14
+ iff: "implies",
15
+ };
16
+ /**
17
+ * Low-level manager for a flat-stored expression tree.
18
+ *
19
+ * Expressions are immutable value objects stored in three maps: the main
20
+ * expression store, a parent-to-children ID index, and a parent-to-positions
21
+ * index. Structural invariants (child limits, root-only operators, position
22
+ * uniqueness) are enforced on every mutation.
23
+ *
24
+ * This class is an internal building block used by {@link PremiseEngine}
25
+ * and is not part of the public API.
26
+ */
27
+ export class ExpressionManager {
28
+ expressions;
29
+ childExpressionIdsByParentId;
30
+ childPositionsByParentId;
31
+ positionConfig;
32
+ config;
33
+ generateId;
34
+ collector = null;
35
+ dirtyExpressionIds = new Set();
36
+ setCollector(collector) {
37
+ this.collector = collector;
38
+ }
39
+ constructor(config) {
40
+ this.expressions = new Map();
41
+ this.childExpressionIdsByParentId = new Map();
42
+ this.childPositionsByParentId = new Map();
43
+ this.positionConfig = config?.positionConfig ?? DEFAULT_POSITION_CONFIG;
44
+ this.config = config;
45
+ this.generateId = config?.generateId ?? defaultGenerateId;
46
+ }
47
+ get grammarConfig() {
48
+ return this.config?.grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
49
+ }
50
+ attachChecksum(expr) {
51
+ const fields = this.config?.checksumConfig?.expressionFields ??
52
+ DEFAULT_CHECKSUM_CONFIG.expressionFields;
53
+ const checksum = entityChecksum(expr, fields);
54
+ return {
55
+ ...expr,
56
+ checksum,
57
+ descendantChecksum: null,
58
+ combinedChecksum: checksum,
59
+ };
60
+ }
61
+ /**
62
+ * Creates and registers a formula-buffer expression in the three internal
63
+ * maps (`expressions`, `childExpressionIdsByParentId`,
64
+ * `childPositionsByParentId`) and notifies the change collector.
65
+ *
66
+ * Used by `addExpression`, `insertExpression`, and `wrapExpression` to
67
+ * auto-insert formula nodes between operators when
68
+ * `grammarConfig.autoNormalize` is enabled.
69
+ *
70
+ * @returns The generated formula expression ID.
71
+ */
72
+ registerFormulaBuffer(sourceExpr, parentId, position, formulaId) {
73
+ formulaId ??= this.generateId();
74
+ const formulaExpr = this.attachChecksum({
75
+ id: formulaId,
76
+ type: "formula",
77
+ argumentId: sourceExpr.argumentId,
78
+ argumentVersion: sourceExpr.argumentVersion,
79
+ premiseId: sourceExpr
80
+ .premiseId,
81
+ parentId,
82
+ position,
83
+ });
84
+ this.expressions.set(formulaId, formulaExpr);
85
+ this.collector?.addedExpression({
86
+ ...formulaExpr,
87
+ });
88
+ getOrCreate(this.childExpressionIdsByParentId, parentId, () => new Set()).add(formulaId);
89
+ getOrCreate(this.childPositionsByParentId, parentId, () => new Set()).add(position);
90
+ return formulaId;
91
+ }
92
+ /**
93
+ * Removes an expression from the three internal maps: deletes it from
94
+ * the main `expressions` store, removes it from its parent's child-id
95
+ * and position indexes, and deletes its own child-id and position
96
+ * indexes.
97
+ *
98
+ * Callers remain responsible for collector notification, dirty-set
99
+ * cleanup, and parent dirtying — timing for those varies by call site.
100
+ */
101
+ detachExpression(expressionId, expression) {
102
+ this.expressions.delete(expressionId);
103
+ this.childExpressionIdsByParentId
104
+ .get(expression.parentId)
105
+ ?.delete(expressionId);
106
+ this.childPositionsByParentId
107
+ .get(expression.parentId)
108
+ ?.delete(expression.position);
109
+ this.childExpressionIdsByParentId.delete(expressionId);
110
+ this.childPositionsByParentId.delete(expressionId);
111
+ }
112
+ /**
113
+ * Marks an expression and all its ancestors as dirty for hierarchical
114
+ * checksum recomputation. Stops early when it reaches an expression
115
+ * already in the dirty set (since its ancestors are already marked).
116
+ */
117
+ markExpressionDirty(exprId) {
118
+ let current = exprId;
119
+ while (current !== null) {
120
+ if (this.dirtyExpressionIds.has(current))
121
+ break; // ancestors already dirty
122
+ this.dirtyExpressionIds.add(current);
123
+ const expr = this.expressions.get(current);
124
+ current = expr ? expr.parentId : null;
125
+ }
126
+ }
127
+ /**
128
+ * Recomputes `descendantChecksum` and `combinedChecksum` for all dirty
129
+ * expressions, processing bottom-up (deepest first) so that children
130
+ * are up-to-date before their parents are computed.
131
+ */
132
+ flushExpressionChecksums() {
133
+ if (this.dirtyExpressionIds.size === 0)
134
+ return;
135
+ // Sort dirty expressions by depth (deepest first) for bottom-up processing
136
+ const dirtyIds = [...this.dirtyExpressionIds];
137
+ const depthOf = (id) => {
138
+ let depth = 0;
139
+ let current = this.expressions.get(id);
140
+ while (current && current.parentId !== null) {
141
+ depth++;
142
+ current = this.expressions.get(current.parentId);
143
+ }
144
+ return depth;
145
+ };
146
+ dirtyIds.sort((a, b) => depthOf(b) - depthOf(a));
147
+ const fields = this.config?.checksumConfig?.expressionFields ??
148
+ DEFAULT_CHECKSUM_CONFIG.expressionFields;
149
+ for (const id of dirtyIds) {
150
+ const expr = this.expressions.get(id);
151
+ if (!expr)
152
+ continue;
153
+ const oldChecksum = expr.checksum;
154
+ const oldDescendantChecksum = expr.descendantChecksum;
155
+ const oldCombinedChecksum = expr.combinedChecksum;
156
+ const metaChecksum = entityChecksum(expr, fields);
157
+ const childIds = this.childExpressionIdsByParentId.get(id);
158
+ let descendantChecksum = null;
159
+ if (childIds && childIds.size > 0) {
160
+ const childMap = {};
161
+ for (const childId of childIds) {
162
+ const child = this.expressions.get(childId);
163
+ if (child) {
164
+ childMap[childId] = child.combinedChecksum;
165
+ }
166
+ }
167
+ descendantChecksum = computeHash(canonicalSerialize(childMap));
168
+ }
169
+ const combinedChecksum = descendantChecksum === null
170
+ ? metaChecksum
171
+ : computeHash(metaChecksum + descendantChecksum);
172
+ this.expressions.set(id, {
173
+ ...expr,
174
+ checksum: metaChecksum,
175
+ descendantChecksum,
176
+ combinedChecksum,
177
+ });
178
+ if (this.collector &&
179
+ !this.collector.isExpressionAdded(expr.id) &&
180
+ (metaChecksum !== oldChecksum ||
181
+ descendantChecksum !== oldDescendantChecksum ||
182
+ combinedChecksum !== oldCombinedChecksum)) {
183
+ this.collector.modifiedExpression({
184
+ ...expr,
185
+ checksum: metaChecksum,
186
+ descendantChecksum,
187
+ combinedChecksum,
188
+ });
189
+ }
190
+ }
191
+ this.dirtyExpressionIds.clear();
192
+ }
193
+ /**
194
+ * Removes deleted expression IDs from the dirty set so that flush
195
+ * doesn't attempt to process expressions that no longer exist.
196
+ */
197
+ pruneDeletedFromDirtySet(deletedIds) {
198
+ for (const id of deletedIds) {
199
+ this.dirtyExpressionIds.delete(id);
200
+ }
201
+ }
202
+ /** Returns all expressions sorted by ID for deterministic output. */
203
+ toArray() {
204
+ return Array.from(this.expressions.values()).sort((a, b) => a.id.localeCompare(b.id));
205
+ }
206
+ /**
207
+ * Adds an expression to the tree.
208
+ *
209
+ * @throws If the expression ID already exists.
210
+ * @throws If the expression references itself as parent.
211
+ * @throws If `implies`/`iff` operators have a non-null parentId (they must be roots).
212
+ * @throws If the parent does not exist or is not an operator/formula.
213
+ * @throws If the parent's child limit would be exceeded.
214
+ * @throws If the position is already occupied under the parent.
215
+ */
216
+ addExpression(input) {
217
+ let expression = input;
218
+ if (this.expressions.has(expression.id)) {
219
+ throw new Error(`Expression with ID "${expression.id}" already exists.`);
220
+ }
221
+ if (expression.parentId === expression.id) {
222
+ throw new Error(`Expression "${expression.id}" cannot be its own parent.`);
223
+ }
224
+ if (expression.type === "operator" &&
225
+ (expression.operator === "implies" ||
226
+ expression.operator === "iff") &&
227
+ expression.parentId !== null) {
228
+ throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
229
+ }
230
+ if (expression.parentId !== null) {
231
+ let parent = this.expressions.get(expression.parentId);
232
+ if (!parent) {
233
+ throw new Error(`Parent expression "${expression.parentId}" does not exist.`);
234
+ }
235
+ if (parent.type !== "operator" && parent.type !== "formula") {
236
+ throw new Error(`Parent expression "${expression.parentId}" is not an operator expression.`);
237
+ }
238
+ // Non-not operators cannot be direct children of operators.
239
+ if (this.grammarConfig.enforceFormulaBetweenOperators &&
240
+ parent.type === "operator" &&
241
+ expression.type === "operator" &&
242
+ expression.operator !== "not") {
243
+ if (this.grammarConfig.autoNormalize) {
244
+ // Check original parent can accept the formula as a new child.
245
+ this.assertChildLimit(parent.operator, expression.parentId);
246
+ // Auto-insert a formula buffer between parent and expression.
247
+ const formulaId = this.registerFormulaBuffer(expression, expression.parentId, expression.position);
248
+ // Rewrite expression to be child of formula.
249
+ expression = {
250
+ ...expression,
251
+ parentId: formulaId,
252
+ position: 0,
253
+ };
254
+ // Update parent reference for subsequent checks.
255
+ parent = this.expressions.get(formulaId);
256
+ }
257
+ else {
258
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
259
+ }
260
+ }
261
+ if (parent.type === "operator") {
262
+ this.assertChildLimit(parent.operator, expression.parentId);
263
+ }
264
+ else {
265
+ const childCount = this.childExpressionIdsByParentId.get(expression.parentId)
266
+ ?.size ?? 0;
267
+ if (childCount >= 1) {
268
+ throw new Error(`Formula expression "${expression.parentId}" can only have one child.`);
269
+ }
270
+ }
271
+ }
272
+ const occupiedPositions = getOrCreate(this.childPositionsByParentId, expression.parentId, () => new Set());
273
+ if (occupiedPositions.has(expression.position)) {
274
+ throw new Error(`Position ${expression.position} is already used under parent "${expression.parentId}".`);
275
+ }
276
+ occupiedPositions.add(expression.position);
277
+ const withChecksum = this.attachChecksum(expression);
278
+ this.expressions.set(expression.id, withChecksum);
279
+ this.collector?.addedExpression({
280
+ ...withChecksum,
281
+ });
282
+ getOrCreate(this.childExpressionIdsByParentId, expression.parentId, () => new Set()).add(expression.id);
283
+ // Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
284
+ this.markExpressionDirty(expression.id);
285
+ }
286
+ /**
287
+ * Adds an expression as the last child of the given parent.
288
+ *
289
+ * If the parent has no children, the expression gets `POSITION_INITIAL`.
290
+ * Otherwise it gets a midpoint between the last child's position and
291
+ * `POSITION_MAX`.
292
+ */
293
+ appendExpression(parentId, expression) {
294
+ const children = this.getChildExpressions(parentId);
295
+ const position = children.length === 0
296
+ ? this.positionConfig.initial
297
+ : midpoint(children[children.length - 1].position, this.positionConfig.max);
298
+ this.addExpression({
299
+ ...expression,
300
+ parentId,
301
+ position,
302
+ });
303
+ }
304
+ /**
305
+ * Adds an expression immediately before or after an existing sibling.
306
+ *
307
+ * @throws If the sibling does not exist.
308
+ */
309
+ addExpressionRelative(siblingId, relativePosition, expression) {
310
+ const sibling = this.expressions.get(siblingId);
311
+ if (!sibling) {
312
+ throw new Error(`Expression "${siblingId}" not found.`);
313
+ }
314
+ const children = this.getChildExpressions(sibling.parentId);
315
+ const siblingIndex = children.findIndex((c) => c.id === siblingId);
316
+ let position;
317
+ if (relativePosition === "before") {
318
+ const prevPosition = siblingIndex > 0
319
+ ? children[siblingIndex - 1].position
320
+ : this.positionConfig.min;
321
+ position = midpoint(prevPosition, sibling.position);
322
+ }
323
+ else {
324
+ const nextPosition = siblingIndex < children.length - 1
325
+ ? children[siblingIndex + 1].position
326
+ : this.positionConfig.max;
327
+ position = midpoint(sibling.position, nextPosition);
328
+ }
329
+ this.addExpression({
330
+ ...expression,
331
+ parentId: sibling.parentId,
332
+ position,
333
+ });
334
+ }
335
+ /**
336
+ * Updates mutable fields of an existing expression in-place.
337
+ *
338
+ * Only `position`, `variableId`, and `operator` may be updated. Structural
339
+ * fields (`id`, `parentId`, `type`, `argumentId`, `argumentVersion`,
340
+ * `checksum`) are forbidden.
341
+ *
342
+ * Operator changes are restricted to permitted swaps: `and`/`or` and
343
+ * `implies`/`iff`. Variable ID changes require the expression to be of
344
+ * type `"variable"`.
345
+ *
346
+ * @throws If the expression does not exist.
347
+ * @throws If a forbidden field is present in `updates`.
348
+ * @throws If an operator change is not permitted.
349
+ * @throws If `variableId` is set on a non-variable expression.
350
+ * @throws If the new position collides with a sibling.
351
+ */
352
+ updateExpression(expressionId, updates) {
353
+ const expression = this.expressions.get(expressionId);
354
+ if (!expression) {
355
+ throw new Error(`Expression "${expressionId}" not found.`);
356
+ }
357
+ // Reject forbidden fields passed via `as any`.
358
+ const FORBIDDEN_KEYS = [
359
+ "id",
360
+ "argumentId",
361
+ "argumentVersion",
362
+ "premiseId",
363
+ "checksum",
364
+ "parentId",
365
+ "type",
366
+ ];
367
+ for (const key of FORBIDDEN_KEYS) {
368
+ if (key in updates) {
369
+ throw new Error(`Field "${key}" is forbidden in expression updates.`);
370
+ }
371
+ }
372
+ // If no actual mutable fields are set, return the expression as-is.
373
+ if (updates.position === undefined &&
374
+ updates.variableId === undefined &&
375
+ updates.operator === undefined) {
376
+ return expression;
377
+ }
378
+ // Validate operator change.
379
+ if (updates.operator !== undefined) {
380
+ if (expression.type !== "operator") {
381
+ throw new Error(`Expression "${expressionId}" is not an operator expression; cannot update operator.`);
382
+ }
383
+ const permitted = PERMITTED_OPERATOR_SWAPS[expression.operator];
384
+ if (permitted !== updates.operator) {
385
+ throw new Error(`Changing operator from "${expression.operator}" to "${updates.operator}" is not a permitted operator change. Permitted: and↔or, implies↔iff.`);
386
+ }
387
+ }
388
+ // Validate variableId change.
389
+ if (updates.variableId !== undefined) {
390
+ if (expression.type !== "variable") {
391
+ throw new Error(`Expression "${expressionId}" is not a variable expression; cannot update variableId.`);
392
+ }
393
+ }
394
+ // Validate position change.
395
+ if (updates.position !== undefined) {
396
+ const positionSet = this.childPositionsByParentId.get(expression.parentId);
397
+ if (positionSet) {
398
+ positionSet.delete(expression.position);
399
+ if (positionSet.has(updates.position)) {
400
+ // Restore old position before throwing.
401
+ positionSet.add(expression.position);
402
+ throw new Error(`Position ${updates.position} is already used under parent "${expression.parentId}".`);
403
+ }
404
+ positionSet.add(updates.position);
405
+ }
406
+ }
407
+ // Build an updated copy and replace in the map.
408
+ const updated = this.attachChecksum({
409
+ ...expression,
410
+ ...(updates.position !== undefined
411
+ ? { position: updates.position }
412
+ : {}),
413
+ ...(updates.variableId !== undefined
414
+ ? { variableId: updates.variableId }
415
+ : {}),
416
+ ...(updates.operator !== undefined
417
+ ? { operator: updates.operator }
418
+ : {}),
419
+ });
420
+ this.expressions.set(expressionId, updated);
421
+ this.collector?.modifiedExpression({
422
+ ...updated,
423
+ });
424
+ // Mark the updated expression and its ancestors dirty for hierarchical checksum recomputation.
425
+ this.markExpressionDirty(expressionId);
426
+ return updated;
427
+ }
428
+ /**
429
+ * Removes an expression from the tree.
430
+ *
431
+ * When `deleteSubtree` is `true`, the expression and its entire descendant
432
+ * subtree are removed, then {@link collapseIfNeeded} runs on the parent.
433
+ *
434
+ * When `deleteSubtree` is `false`, the expression is removed but its single
435
+ * child (if any) is promoted into the removed expression's slot. If the
436
+ * expression has more than one child, an error is thrown. Leaf removal
437
+ * (0 children) still triggers {@link collapseIfNeeded} on the parent.
438
+ * Promotion does **not** trigger collapse.
439
+ *
440
+ * @throws If `deleteSubtree` is `false` and the expression has multiple children.
441
+ * @throws If `deleteSubtree` is `false` and the single child is a root-only
442
+ * operator (`implies`/`iff`) that would be placed in a non-root position.
443
+ * @returns The removed expression, or `undefined` if not found.
444
+ */
445
+ removeExpression(expressionId, deleteSubtree) {
446
+ const target = this.expressions.get(expressionId);
447
+ if (!target) {
448
+ return undefined;
449
+ }
450
+ // Pre-flight: simulate collapse chain to detect nesting/root-only violations.
451
+ this.assertRemovalSafe(expressionId, deleteSubtree);
452
+ if (deleteSubtree) {
453
+ return this.removeSubtree(expressionId, target);
454
+ }
455
+ else {
456
+ return this.removeAndPromote(expressionId, target);
457
+ }
458
+ }
459
+ removeSubtree(expressionId, target) {
460
+ const parentId = target.parentId;
461
+ const toRemove = new Set();
462
+ const stack = [expressionId];
463
+ while (stack.length > 0) {
464
+ const currentId = stack.pop();
465
+ if (!currentId || toRemove.has(currentId)) {
466
+ continue;
467
+ }
468
+ toRemove.add(currentId);
469
+ const children = this.childExpressionIdsByParentId.get(currentId);
470
+ if (!children) {
471
+ continue;
472
+ }
473
+ for (const childId of children) {
474
+ stack.push(childId);
475
+ }
476
+ }
477
+ for (const id of toRemove) {
478
+ const expression = this.expressions.get(id);
479
+ if (!expression) {
480
+ continue;
481
+ }
482
+ this.collector?.removedExpression({
483
+ ...expression,
484
+ });
485
+ this.detachExpression(id, expression);
486
+ }
487
+ // Prune deleted expressions from the dirty set and mark the surviving parent dirty.
488
+ this.pruneDeletedFromDirtySet(toRemove);
489
+ if (parentId !== null) {
490
+ this.markExpressionDirty(parentId);
491
+ }
492
+ this.collapseIfNeeded(parentId);
493
+ return target;
494
+ }
495
+ removeAndPromote(expressionId, target) {
496
+ const children = this.getChildExpressions(expressionId);
497
+ if (children.length > 1) {
498
+ throw new Error(`Cannot promote: expression "${expressionId}" has multiple children (${children.length}). Use deleteSubtree: true or remove children first.`);
499
+ }
500
+ if (children.length === 0) {
501
+ // Leaf removal — same as removing a single node, then collapse parent.
502
+ const parentId = target.parentId;
503
+ this.collector?.removedExpression({
504
+ ...target,
505
+ });
506
+ this.detachExpression(expressionId, target);
507
+ // Prune deleted expression from dirty set and mark surviving parent dirty.
508
+ this.dirtyExpressionIds.delete(expressionId);
509
+ if (parentId !== null) {
510
+ this.markExpressionDirty(parentId);
511
+ }
512
+ this.collapseIfNeeded(parentId);
513
+ return target;
514
+ }
515
+ // Exactly 1 child — promote it into the target's slot.
516
+ const child = children[0];
517
+ // Validate: non-not operators cannot be promoted into an operator parent.
518
+ if (this.grammarConfig.enforceFormulaBetweenOperators) {
519
+ if (child.type === "operator" &&
520
+ child.operator !== "not" &&
521
+ target.parentId !== null) {
522
+ const grandparent = this.expressions.get(target.parentId);
523
+ if (grandparent && grandparent.type === "operator") {
524
+ throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
525
+ }
526
+ }
527
+ }
528
+ // Validate: root-only operators cannot be promoted into a non-root position.
529
+ if (child.type === "operator" &&
530
+ (child.operator === "implies" || child.operator === "iff") &&
531
+ target.parentId !== null) {
532
+ throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
533
+ }
534
+ // Promote child into the target's slot.
535
+ const promoted = this.attachChecksum({
536
+ ...child,
537
+ parentId: target.parentId,
538
+ position: target.position,
539
+ });
540
+ this.expressions.set(child.id, promoted);
541
+ // Update parent's child-id set: remove target, add promoted child.
542
+ this.childExpressionIdsByParentId
543
+ .get(target.parentId)
544
+ ?.delete(expressionId);
545
+ getOrCreate(this.childExpressionIdsByParentId, target.parentId, () => new Set()).add(child.id);
546
+ // The parent's position set is unchanged: target.position was already
547
+ // tracked and continues to be occupied by the promoted child.
548
+ // Clean up target's own tracking entries.
549
+ this.childExpressionIdsByParentId.delete(expressionId);
550
+ this.childPositionsByParentId.delete(expressionId);
551
+ // Notify collector.
552
+ this.collector?.removedExpression({
553
+ ...target,
554
+ });
555
+ this.collector?.modifiedExpression({
556
+ ...promoted,
557
+ });
558
+ // Remove target from expressions map.
559
+ this.expressions.delete(expressionId);
560
+ // Prune deleted expression from dirty set and mark promoted child dirty
561
+ // (its parentId changed) which also propagates to ancestors.
562
+ this.dirtyExpressionIds.delete(expressionId);
563
+ this.markExpressionDirty(child.id);
564
+ // After promotion, the target's parent may be a formula that now needs collapsing
565
+ // (e.g., if the promoted child has no binary operator in its bounded subtree).
566
+ this.collapseIfNeeded(target.parentId);
567
+ return target;
568
+ }
569
+ /**
570
+ * Promotes `child` into the slot occupied by `parent` and removes `parent`.
571
+ * Used by `collapseIfNeeded` and `normalize()`.
572
+ */
573
+ promoteChild(parentId, parent, child) {
574
+ const grandparentId = parent.parentId;
575
+ const grandparentPosition = parent.position;
576
+ const promoted = this.attachChecksum({
577
+ ...child,
578
+ parentId: grandparentId,
579
+ position: grandparentPosition,
580
+ });
581
+ this.expressions.set(child.id, promoted);
582
+ this.collector?.modifiedExpression({
583
+ ...promoted,
584
+ });
585
+ this.childExpressionIdsByParentId.get(grandparentId)?.delete(parentId);
586
+ getOrCreate(this.childExpressionIdsByParentId, grandparentId, () => new Set()).add(child.id);
587
+ this.childExpressionIdsByParentId.delete(parentId);
588
+ this.childPositionsByParentId.delete(parentId);
589
+ this.collector?.removedExpression({
590
+ ...parent,
591
+ });
592
+ this.expressions.delete(parentId);
593
+ this.dirtyExpressionIds.delete(parentId);
594
+ this.markExpressionDirty(child.id);
595
+ }
596
+ collapseIfNeeded(operatorId) {
597
+ if (!this.grammarConfig.autoNormalize)
598
+ return;
599
+ if (operatorId === null)
600
+ return;
601
+ const operator = this.expressions.get(operatorId);
602
+ if (!operator)
603
+ return;
604
+ if (operator.type === "formula") {
605
+ const children = this.getChildExpressions(operatorId);
606
+ if (children.length === 0) {
607
+ const grandparentId = operator.parentId;
608
+ this.collector?.removedExpression({
609
+ ...operator,
610
+ });
611
+ this.detachExpression(operatorId, operator);
612
+ this.dirtyExpressionIds.delete(operatorId);
613
+ if (grandparentId !== null) {
614
+ this.markExpressionDirty(grandparentId);
615
+ }
616
+ this.collapseIfNeeded(grandparentId);
617
+ return;
618
+ }
619
+ // 1-child formula: collapse if no binary operator in bounded subtree.
620
+ if (children.length === 1 &&
621
+ !this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
622
+ const grandparentId = operator.parentId;
623
+ this.promoteChild(operatorId, operator, children[0]);
624
+ // Grandparent may also be a formula that now needs collapsing.
625
+ this.collapseIfNeeded(grandparentId);
626
+ }
627
+ return;
628
+ }
629
+ if (operator.type !== "operator")
630
+ return;
631
+ const children = this.getChildExpressions(operatorId);
632
+ if (children.length === 0) {
633
+ const grandparentId = operator.parentId;
634
+ this.collector?.removedExpression({
635
+ ...operator,
636
+ });
637
+ this.detachExpression(operatorId, operator);
638
+ // Prune collapsed operator from dirty set and propagate to grandparent.
639
+ this.dirtyExpressionIds.delete(operatorId);
640
+ if (grandparentId !== null) {
641
+ this.markExpressionDirty(grandparentId);
642
+ }
643
+ this.collapseIfNeeded(grandparentId);
644
+ }
645
+ else if (children.length === 1 && operator.operator === "not") {
646
+ // `not` is unary — 1 child is its valid state; skip collapse.
647
+ // Still recurse to grandparent: a formula wrapping this `not` may
648
+ // now qualify for collapse after a descendant change.
649
+ this.collapseIfNeeded(operator.parentId);
650
+ }
651
+ else if (children.length === 1) {
652
+ const child = children[0];
653
+ const grandparentId = operator.parentId;
654
+ // Defense-in-depth: validate promotion doesn't violate nesting or root-only rules.
655
+ if (child.type === "operator") {
656
+ // Root-only — always enforced
657
+ if ((child.operator === "implies" ||
658
+ child.operator === "iff") &&
659
+ grandparentId !== null) {
660
+ throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
661
+ }
662
+ // Nesting — grammar-configurable
663
+ if (this.grammarConfig.enforceFormulaBetweenOperators) {
664
+ if (child.operator !== "not" && grandparentId !== null) {
665
+ const grandparent = this.expressions.get(grandparentId);
666
+ if (grandparent && grandparent.type === "operator") {
667
+ throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
668
+ }
669
+ }
670
+ }
671
+ }
672
+ this.promoteChild(operatorId, operator, child);
673
+ // Grandparent may be a formula that now needs collapsing after the
674
+ // promoted child replaced the operator.
675
+ this.collapseIfNeeded(grandparentId);
676
+ }
677
+ }
678
+ /**
679
+ * Checks whether the subtree rooted at `expressionId` contains a binary
680
+ * operator (`and` or `or`). Traversal stops at formula boundaries — a
681
+ * nested formula owns its own subtree and is not inspected.
682
+ */
683
+ hasBinaryOperatorInBoundedSubtree(expressionId) {
684
+ const expr = this.expressions.get(expressionId);
685
+ if (!expr)
686
+ return false;
687
+ if (expr.type === "formula")
688
+ return false;
689
+ if (expr.type === "variable")
690
+ return false;
691
+ if (expr.type === "operator" &&
692
+ (expr.operator === "and" || expr.operator === "or")) {
693
+ return true;
694
+ }
695
+ const children = this.getChildExpressions(expressionId);
696
+ return children.some((child) => this.hasBinaryOperatorInBoundedSubtree(child.id));
697
+ }
698
+ /**
699
+ * Performs a full normalization sweep on the expression tree:
700
+ * 1. Collapses operators with 0 or 1 children.
701
+ * 2. Collapses formulas whose bounded subtree has no binary operator.
702
+ * 3. Inserts formula buffers where `enforceFormulaBetweenOperators` requires them.
703
+ * 4. Repeats until stable.
704
+ *
705
+ * Works regardless of the current `autoNormalize` setting — this is an
706
+ * explicit on-demand normalization.
707
+ */
708
+ normalize() {
709
+ let changed = true;
710
+ while (changed) {
711
+ changed = false;
712
+ // Pass 1: Collapse operators with 0 or 1 children (bottom-up).
713
+ for (const expr of this.toArray()) {
714
+ if (expr.type !== "operator")
715
+ continue;
716
+ if (!this.expressions.has(expr.id))
717
+ continue;
718
+ const children = this.getChildExpressions(expr.id);
719
+ if (children.length === 0) {
720
+ const grandparentId = expr.parentId;
721
+ this.collector?.removedExpression({
722
+ ...expr,
723
+ });
724
+ this.detachExpression(expr.id, expr);
725
+ this.dirtyExpressionIds.delete(expr.id);
726
+ if (grandparentId !== null) {
727
+ this.markExpressionDirty(grandparentId);
728
+ }
729
+ changed = true;
730
+ }
731
+ else if (children.length === 1 && expr.operator !== "not") {
732
+ this.promoteChild(expr.id, expr, children[0]);
733
+ changed = true;
734
+ }
735
+ }
736
+ // Pass 2: Collapse unjustified formulas (bottom-up).
737
+ for (const expr of this.toArray()) {
738
+ if (expr.type !== "formula")
739
+ continue;
740
+ if (!this.expressions.has(expr.id))
741
+ continue;
742
+ const children = this.getChildExpressions(expr.id);
743
+ if (children.length === 0) {
744
+ const grandparentId = expr.parentId;
745
+ this.collector?.removedExpression({
746
+ ...expr,
747
+ });
748
+ this.detachExpression(expr.id, expr);
749
+ this.dirtyExpressionIds.delete(expr.id);
750
+ if (grandparentId !== null) {
751
+ this.markExpressionDirty(grandparentId);
752
+ }
753
+ changed = true;
754
+ }
755
+ else if (children.length === 1 &&
756
+ !this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
757
+ this.promoteChild(expr.id, expr, children[0]);
758
+ changed = true;
759
+ }
760
+ }
761
+ // Pass 3: Insert formula buffers for operator-under-operator violations.
762
+ for (const expr of this.toArray()) {
763
+ if (expr.type !== "operator" || expr.operator === "not")
764
+ continue;
765
+ if (!this.expressions.has(expr.id))
766
+ continue;
767
+ if (expr.parentId === null)
768
+ continue;
769
+ const parent = this.expressions.get(expr.parentId);
770
+ if (!parent || parent.type !== "operator")
771
+ continue;
772
+ // Non-not operator is direct child of operator — insert formula buffer.
773
+ const formulaId = this.registerFormulaBuffer(expr, expr.parentId, expr.position);
774
+ // Reparent the operator under the formula.
775
+ this.reparent(expr.id, formulaId, 0);
776
+ changed = true;
777
+ }
778
+ }
779
+ }
780
+ /** Returns `true` if any expression in the tree references the given variable ID. */
781
+ hasVariableReference(variableId) {
782
+ for (const expression of this.expressions.values()) {
783
+ if (expression.type === "variable" &&
784
+ expression.variableId === variableId) {
785
+ return true;
786
+ }
787
+ }
788
+ return false;
789
+ }
790
+ /** Returns the expression with the given ID, or `undefined` if not found. */
791
+ getExpression(expressionId) {
792
+ return this.expressions.get(expressionId);
793
+ }
794
+ /** Returns the children of the given parent, sorted by position. */
795
+ getChildExpressions(parentId) {
796
+ const childIds = this.childExpressionIdsByParentId.get(parentId);
797
+ if (!childIds || childIds.size === 0) {
798
+ return [];
799
+ }
800
+ const children = [];
801
+ for (const childId of childIds) {
802
+ const child = this.expressions.get(childId);
803
+ if (child) {
804
+ children.push(child);
805
+ }
806
+ }
807
+ return children.sort((a, b) => a.position - b.position);
808
+ }
809
+ loadInitialExpressions(initialExpressions) {
810
+ if (initialExpressions.length === 0) {
811
+ return;
812
+ }
813
+ const pending = new Map(initialExpressions.map((expression) => [expression.id, expression]));
814
+ let progressed = true;
815
+ while (pending.size > 0 && progressed) {
816
+ progressed = false;
817
+ for (const [id, expression] of Array.from(pending.entries())) {
818
+ if (expression.parentId !== null &&
819
+ !this.expressions.has(expression.parentId)) {
820
+ continue;
821
+ }
822
+ this.addExpression(expression);
823
+ pending.delete(id);
824
+ progressed = true;
825
+ }
826
+ }
827
+ if (pending.size > 0) {
828
+ const unresolved = Array.from(pending.keys()).join(", ");
829
+ throw new Error(`Could not resolve parent relationships for expressions: ${unresolved}.`);
830
+ }
831
+ }
832
+ /**
833
+ * Simulates the collapse chain that would result from removing an expression.
834
+ * Throws if any promotion would violate nesting or root-only rules.
835
+ */
836
+ assertRemovalSafe(expressionId, deleteSubtree) {
837
+ const target = this.expressions.get(expressionId);
838
+ if (!target)
839
+ return;
840
+ if (!deleteSubtree) {
841
+ const children = this.getChildExpressions(expressionId);
842
+ // >1 children: removeAndPromote throws before any mutation, no nesting concern.
843
+ if (children.length === 1) {
844
+ this.assertPromotionSafe(children[0], target.parentId);
845
+ // Simulate post-promotion cascade (formula collapse after promotion).
846
+ if (this.grammarConfig.autoNormalize) {
847
+ this.simulatePostPromotionCollapse(target.parentId, children[0]);
848
+ }
849
+ }
850
+ if (children.length === 0) {
851
+ this.simulateCollapseChain(target.parentId, expressionId);
852
+ }
853
+ return;
854
+ }
855
+ // deleteSubtree: entire subtree removed, then collapse runs on parent.
856
+ this.simulateCollapseChain(target.parentId, expressionId);
857
+ }
858
+ /**
859
+ * Checks whether promoting `child` into a slot with the given `newParentId`
860
+ * would violate the nesting rule or root-only rule.
861
+ */
862
+ assertPromotionSafe(child, newParentId) {
863
+ if (child.type !== "operator")
864
+ return;
865
+ // Root-only check — always enforced
866
+ if ((child.operator === "implies" || child.operator === "iff") &&
867
+ newParentId !== null) {
868
+ throw new Error(`Cannot remove expression — would promote a root-only operator ("${child.operator}") to a non-root position`);
869
+ }
870
+ // Nesting check — grammar-configurable
871
+ if (this.grammarConfig.enforceFormulaBetweenOperators) {
872
+ if (child.operator !== "not" && newParentId !== null) {
873
+ const newParent = this.expressions.get(newParentId);
874
+ if (newParent && newParent.type === "operator") {
875
+ throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
876
+ }
877
+ }
878
+ }
879
+ }
880
+ /**
881
+ * Walks the collapse chain starting from `operatorId` after `removedChildId`
882
+ * is removed. At each level: if 0 remaining children, operator/formula is deleted
883
+ * and chain continues up. If 1 remaining child, check promotion safety.
884
+ */
885
+ simulateCollapseChain(operatorId, removedChildId) {
886
+ if (!this.grammarConfig.autoNormalize)
887
+ return;
888
+ if (operatorId === null)
889
+ return;
890
+ const operator = this.expressions.get(operatorId);
891
+ if (!operator)
892
+ return;
893
+ if (operator.type !== "operator" && operator.type !== "formula")
894
+ return;
895
+ const children = this.getChildExpressions(operatorId);
896
+ const remainingChildren = children.filter((c) => c.id !== removedChildId);
897
+ if (operator.type === "formula") {
898
+ if (remainingChildren.length === 0) {
899
+ this.simulateCollapseChain(operator.parentId, operatorId);
900
+ }
901
+ else if (remainingChildren.length === 1 &&
902
+ !this.hasBinaryOperatorInBoundedSubtree(remainingChildren[0].id)) {
903
+ // Formula would collapse — child promoted.
904
+ // Formula collapse promotion is always safe (child is variable, not, or formula).
905
+ this.simulateCollapseChain(operator.parentId, operatorId);
906
+ }
907
+ return;
908
+ }
909
+ // operator.type === "operator"
910
+ if (remainingChildren.length === 0) {
911
+ this.simulateCollapseChain(operator.parentId, operatorId);
912
+ }
913
+ else if (remainingChildren.length === 1) {
914
+ this.assertPromotionSafe(remainingChildren[0], operator.parentId);
915
+ // After promotion, simulate further collapse on grandparent.
916
+ this.simulatePostPromotionCollapse(operator.parentId, remainingChildren[0]);
917
+ }
918
+ }
919
+ /**
920
+ * After an operator promotion places `promotedChild` into `parentId`'s child set,
921
+ * check whether the parent (if a formula) would itself collapse. Formula collapse
922
+ * promotion is always safe (the child can't be a binary operator or root-only operator),
923
+ * but we need to continue the simulation chain.
924
+ */
925
+ simulatePostPromotionCollapse(parentId, promotedChild) {
926
+ if (parentId === null)
927
+ return;
928
+ const parent = this.expressions.get(parentId);
929
+ if (!parent)
930
+ return;
931
+ if (parent.type === "formula") {
932
+ if (!this.hasBinaryOperatorInBoundedSubtree(promotedChild.id)) {
933
+ // Formula would collapse. The promotedChild takes formula's slot.
934
+ // This is always safe. Continue simulation from formula's parent.
935
+ this.simulatePostPromotionCollapse(parent.parentId, promotedChild);
936
+ }
937
+ }
938
+ // Operator parents: child count unchanged, no further collapse.
939
+ }
940
+ assertChildLimit(operator, parentId) {
941
+ const childCount = this.childExpressionIdsByParentId.get(parentId)?.size ?? 0;
942
+ if (operator === "not" && childCount >= 1) {
943
+ throw new Error(`Operator expression "${parentId}" with "not" can only have one child.`);
944
+ }
945
+ if ((operator === "implies" || operator === "iff") && childCount >= 2) {
946
+ throw new Error(`Operator expression "${parentId}" with "${operator}" can only have two children.`);
947
+ }
948
+ }
949
+ reparent(expressionId, newParentId, newPosition) {
950
+ const expression = this.expressions.get(expressionId);
951
+ const oldParentId = expression.parentId;
952
+ // Detach from old parent.
953
+ this.childExpressionIdsByParentId
954
+ .get(expression.parentId)
955
+ ?.delete(expressionId);
956
+ this.childPositionsByParentId
957
+ .get(expression.parentId)
958
+ ?.delete(expression.position);
959
+ // Replace the stored value (expressions are immutable value objects).
960
+ const updated = this.attachChecksum({
961
+ ...expression,
962
+ parentId: newParentId,
963
+ position: newPosition,
964
+ });
965
+ this.expressions.set(expressionId, updated);
966
+ this.collector?.modifiedExpression({
967
+ ...updated,
968
+ });
969
+ // Attach to new parent.
970
+ getOrCreate(this.childExpressionIdsByParentId, newParentId, () => new Set()).add(expressionId);
971
+ getOrCreate(this.childPositionsByParentId, newParentId, () => new Set()).add(newPosition);
972
+ // Mark both old and new parent chains dirty for hierarchical checksum recomputation.
973
+ this.markExpressionDirty(expressionId);
974
+ if (oldParentId !== null && oldParentId !== newParentId) {
975
+ this.markExpressionDirty(oldParentId);
976
+ }
977
+ }
978
+ /**
979
+ * Inserts a new expression between existing nodes in the tree.
980
+ *
981
+ * The new expression inherits the tree slot of the anchor node
982
+ * (`leftNodeId ?? rightNodeId`). The anchor and optional second node
983
+ * become children of the new expression at positions 0 and 1.
984
+ *
985
+ * Right node is reparented before left node to handle the case where
986
+ * the right node is a descendant of the left node's subtree.
987
+ *
988
+ * @throws If neither leftNodeId nor rightNodeId is provided.
989
+ * @throws If the expression ID already exists.
990
+ * @throws If leftNodeId and rightNodeId are the same.
991
+ * @throws If either referenced node does not exist.
992
+ * @throws If a unary operator/formula is given two children.
993
+ * @throws If either child is an `implies`/`iff` operator (cannot be subordinated).
994
+ * @throws If an `implies`/`iff` expression would be inserted at a non-root position.
995
+ */
996
+ insertExpression(expression, leftNodeId, rightNodeId) {
997
+ // 1. At least one child node must be provided.
998
+ if (leftNodeId === undefined && rightNodeId === undefined) {
999
+ throw new Error(`insertExpression requires at least one of leftNodeId or rightNodeId.`);
1000
+ }
1001
+ // 2. The new expression's ID must not already exist.
1002
+ if (this.expressions.has(expression.id)) {
1003
+ throw new Error(`Expression with ID "${expression.id}" already exists.`);
1004
+ }
1005
+ // 3. An expression cannot be its own parent.
1006
+ if (expression.parentId === expression.id) {
1007
+ throw new Error(`Expression "${expression.id}" cannot be its own parent.`);
1008
+ }
1009
+ // 4. Left and right nodes must be distinct.
1010
+ if (leftNodeId !== undefined &&
1011
+ rightNodeId !== undefined &&
1012
+ leftNodeId === rightNodeId) {
1013
+ throw new Error(`leftNodeId and rightNodeId must be different.`);
1014
+ }
1015
+ // 5. The left node must exist if provided.
1016
+ // Cast to base TExpressionInput for validation access — deferred conditional
1017
+ // types (TExpressionInput<TExpr>) cannot be narrowed by TS control flow.
1018
+ const leftNode = leftNodeId !== undefined
1019
+ ? this.expressions.get(leftNodeId)
1020
+ : undefined;
1021
+ if (leftNodeId !== undefined && !leftNode) {
1022
+ throw new Error(`Expression "${leftNodeId}" does not exist.`);
1023
+ }
1024
+ // 6. The right node must exist if provided.
1025
+ const rightNode = rightNodeId !== undefined
1026
+ ? this.expressions.get(rightNodeId)
1027
+ : undefined;
1028
+ if (rightNodeId !== undefined && !rightNode) {
1029
+ throw new Error(`Expression "${rightNodeId}" does not exist.`);
1030
+ }
1031
+ // 7a. A variable expression cannot have children.
1032
+ if (expression.type === "variable") {
1033
+ throw new Error(`Variable expression "${expression.id}" cannot have children.`);
1034
+ }
1035
+ // 7. The "not" operator is unary and cannot take two children.
1036
+ if (expression.type === "operator" &&
1037
+ expression.operator === "not" &&
1038
+ leftNodeId !== undefined &&
1039
+ rightNodeId !== undefined) {
1040
+ throw new Error(`Operator expression "${expression.id}" with "not" can only have one child.`);
1041
+ }
1042
+ // 7b. A formula expression is also unary and cannot take two children.
1043
+ if (expression.type === "formula" &&
1044
+ leftNodeId !== undefined &&
1045
+ rightNodeId !== undefined) {
1046
+ throw new Error(`Formula expression "${expression.id}" can only have one child.`);
1047
+ }
1048
+ // 8. The left node must not be an implies/iff expression (which must remain a root).
1049
+ if (leftNode?.type === "operator" &&
1050
+ (leftNode.operator === "implies" || leftNode.operator === "iff")) {
1051
+ throw new Error(`Expression "${leftNodeId}" with "${leftNode.operator}" cannot be subordinated (it must remain a root expression).`);
1052
+ }
1053
+ // 9. The right node must not be an implies/iff expression (which must remain a root).
1054
+ if (rightNode?.type === "operator" &&
1055
+ (rightNode.operator === "implies" || rightNode.operator === "iff")) {
1056
+ throw new Error(`Expression "${rightNodeId}" with "${rightNode.operator}" cannot be subordinated (it must remain a root expression).`);
1057
+ }
1058
+ // The anchor is the node whose current tree slot the new expression will inherit.
1059
+ const anchor = (leftNode ?? rightNode);
1060
+ // 10. implies/iff expressions may only be inserted at the root of the tree.
1061
+ if (expression.type === "operator" &&
1062
+ (expression.operator === "implies" ||
1063
+ expression.operator === "iff") &&
1064
+ anchor.parentId !== null) {
1065
+ throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
1066
+ }
1067
+ // 10a. Non-not operators cannot be direct children of operators.
1068
+ // Track which children need formula buffers (Site 2) for post-reparent insertion.
1069
+ let needsParentFormulaBuffer = false;
1070
+ const childrenNeedingFormulaBuffer = [];
1071
+ if (this.grammarConfig.enforceFormulaBetweenOperators) {
1072
+ // Check 1 (Site 1): new expression as child of anchor's parent.
1073
+ if (anchor.parentId !== null &&
1074
+ expression.type === "operator" &&
1075
+ expression.operator !== "not") {
1076
+ const anchorParent = this.expressions.get(anchor.parentId);
1077
+ if (anchorParent && anchorParent.type === "operator") {
1078
+ if (this.grammarConfig.autoNormalize) {
1079
+ needsParentFormulaBuffer = true;
1080
+ }
1081
+ else {
1082
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1083
+ }
1084
+ }
1085
+ }
1086
+ // Check 2 (Site 2): left/right nodes as children of the new expression.
1087
+ if (expression.type === "operator") {
1088
+ if (leftNode?.type === "operator" &&
1089
+ leftNode.operator !== "not") {
1090
+ if (this.grammarConfig.autoNormalize) {
1091
+ childrenNeedingFormulaBuffer.push(leftNodeId);
1092
+ }
1093
+ else {
1094
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1095
+ }
1096
+ }
1097
+ if (rightNode?.type === "operator" &&
1098
+ rightNode.operator !== "not") {
1099
+ if (this.grammarConfig.autoNormalize) {
1100
+ childrenNeedingFormulaBuffer.push(rightNodeId);
1101
+ }
1102
+ else {
1103
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ const anchorParentId = anchor.parentId;
1109
+ const anchorPosition = anchor.position;
1110
+ // Reparent rightNode first in case it is a descendant of leftNode.
1111
+ if (rightNodeId !== undefined) {
1112
+ this.reparent(rightNodeId, expression.id, 1);
1113
+ }
1114
+ if (leftNodeId !== undefined) {
1115
+ this.reparent(leftNodeId, expression.id, 0);
1116
+ }
1117
+ // Determine the slot for the new expression. If a parent formula buffer
1118
+ // is needed, the formula takes the anchor slot and the expression goes under it.
1119
+ let finalParentId = anchorParentId;
1120
+ let finalPosition = anchorPosition;
1121
+ if (needsParentFormulaBuffer) {
1122
+ const formulaId = this.registerFormulaBuffer(expression, anchorParentId, anchorPosition);
1123
+ finalParentId = formulaId;
1124
+ finalPosition = 0;
1125
+ }
1126
+ // Store the new expression in its slot.
1127
+ const stored = this.attachChecksum({
1128
+ ...expression,
1129
+ parentId: finalParentId,
1130
+ position: finalPosition,
1131
+ });
1132
+ this.expressions.set(expression.id, stored);
1133
+ this.collector?.addedExpression({
1134
+ ...stored,
1135
+ });
1136
+ getOrCreate(this.childExpressionIdsByParentId, finalParentId, () => new Set()).add(expression.id);
1137
+ getOrCreate(this.childPositionsByParentId, finalParentId, () => new Set()).add(finalPosition);
1138
+ // Site 2: auto-insert formula buffers between the new expression and
1139
+ // any offending operator children.
1140
+ for (const childId of childrenNeedingFormulaBuffer) {
1141
+ const child = this.expressions.get(childId);
1142
+ const childPosition = child.position;
1143
+ // Reparent the child under the formula first. This detaches the child
1144
+ // from expression.id's tracking (removing its position from the set).
1145
+ // registerFormulaBuffer then occupies the freed position.
1146
+ const formulaId = this.generateId();
1147
+ this.reparent(childId, formulaId, 0);
1148
+ this.registerFormulaBuffer(expression, expression.id, childPosition, formulaId);
1149
+ }
1150
+ // Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
1151
+ // Note: reparent() already marks children dirty, so this propagates from the new expression up.
1152
+ this.markExpressionDirty(expression.id);
1153
+ }
1154
+ /**
1155
+ * Wraps an existing expression with a new operator and a new sibling.
1156
+ *
1157
+ * The operator takes the existing node's slot in the tree. Both the
1158
+ * existing node and the new sibling become children of the operator.
1159
+ *
1160
+ * Exactly one of `leftNodeId` / `rightNodeId` must be provided — it
1161
+ * identifies the existing node and which child slot (position 0 or 1)
1162
+ * it occupies. The new sibling fills the other slot.
1163
+ *
1164
+ * @throws If neither or both of leftNodeId/rightNodeId are provided.
1165
+ * @throws If the operator or sibling expression ID already exists.
1166
+ * @throws If operator and sibling IDs are the same.
1167
+ * @throws If the existing node does not exist.
1168
+ * @throws If the operator is not of type `"operator"`.
1169
+ * @throws If the operator is unary (`not`).
1170
+ * @throws If the operator is `implies`/`iff` and the existing node is not at root.
1171
+ * @throws If the existing node is an `implies`/`iff` operator (cannot be subordinated).
1172
+ * @throws If the new sibling is an `implies`/`iff` operator (cannot be subordinated).
1173
+ */
1174
+ wrapExpression(operator, newSibling, leftNodeId, rightNodeId) {
1175
+ // 1. Exactly one of leftNodeId / rightNodeId must be provided.
1176
+ if (leftNodeId === undefined && rightNodeId === undefined) {
1177
+ throw new Error(`wrapExpression requires exactly one of leftNodeId or rightNodeId.`);
1178
+ }
1179
+ if (leftNodeId !== undefined && rightNodeId !== undefined) {
1180
+ throw new Error(`wrapExpression requires exactly one of leftNodeId or rightNodeId, not both.`);
1181
+ }
1182
+ // 2. Operator expression ID must not already exist.
1183
+ if (this.expressions.has(operator.id)) {
1184
+ throw new Error(`Expression with ID "${operator.id}" already exists.`);
1185
+ }
1186
+ // 3. New sibling expression ID must not already exist.
1187
+ if (this.expressions.has(newSibling.id)) {
1188
+ throw new Error(`Expression with ID "${newSibling.id}" already exists.`);
1189
+ }
1190
+ // 4. Operator and sibling IDs must be different.
1191
+ if (operator.id === newSibling.id) {
1192
+ throw new Error(`Operator and sibling expression IDs must be different.`);
1193
+ }
1194
+ // 5. The existing node must exist.
1195
+ const existingNodeId = (leftNodeId ?? rightNodeId);
1196
+ const existingNode = this.expressions.get(existingNodeId);
1197
+ if (!existingNode) {
1198
+ throw new Error(`Expression "${existingNodeId}" does not exist.`);
1199
+ }
1200
+ // 6. Operator expression must have type "operator".
1201
+ if (operator.type !== "operator") {
1202
+ throw new Error(`Wrap operator expression "${operator.id}" must have type "operator", got "${operator.type}".`);
1203
+ }
1204
+ // 7. Operator must not be unary ("not").
1205
+ if (operator.operator === "not") {
1206
+ throw new Error(`Operator expression "${operator.id}" with "not" cannot wrap (it is unary and wrapping always produces two children).`);
1207
+ }
1208
+ // 8. implies/iff operator only allowed if existing node is at root.
1209
+ if ((operator.operator === "implies" || operator.operator === "iff") &&
1210
+ existingNode.parentId !== null) {
1211
+ throw new Error(`Operator expression "${operator.id}" with "${operator.operator}" must be a root expression (parentId must be null).`);
1212
+ }
1213
+ // 9. Existing node must not be implies/iff (cannot be subordinated).
1214
+ if (existingNode.type === "operator" &&
1215
+ (existingNode.operator === "implies" ||
1216
+ existingNode.operator === "iff")) {
1217
+ throw new Error(`Expression "${existingNodeId}" with "${existingNode.operator}" cannot be subordinated (it must remain a root expression).`);
1218
+ }
1219
+ // 10. New sibling must not be implies/iff (cannot be subordinated).
1220
+ if (newSibling.type === "operator" &&
1221
+ (newSibling.operator === "implies" || newSibling.operator === "iff")) {
1222
+ throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
1223
+ }
1224
+ // 10a. Non-not operators cannot be direct children of operators.
1225
+ // Track which sites need formula buffers for post-mutation insertion.
1226
+ let needsParentFormulaBuffer = false;
1227
+ let existingNodeNeedsFormulaBuffer = false;
1228
+ let siblingNeedsFormulaBuffer = false;
1229
+ if (this.grammarConfig.enforceFormulaBetweenOperators) {
1230
+ // Check 1 (Site 1): new operator as child of existing node's parent.
1231
+ // Note: step 7 already rejects `not`, so operator.operator is always non-not here.
1232
+ if (existingNode.parentId !== null) {
1233
+ const existingParent = this.expressions.get(existingNode.parentId);
1234
+ if (existingParent && existingParent.type === "operator") {
1235
+ if (this.grammarConfig.autoNormalize) {
1236
+ needsParentFormulaBuffer = true;
1237
+ }
1238
+ else {
1239
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1240
+ }
1241
+ }
1242
+ }
1243
+ // Check 2 (Site 2): existing node as child of new operator.
1244
+ if (existingNode.type === "operator" &&
1245
+ existingNode.operator !== "not") {
1246
+ if (this.grammarConfig.autoNormalize) {
1247
+ existingNodeNeedsFormulaBuffer = true;
1248
+ }
1249
+ else {
1250
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1251
+ }
1252
+ }
1253
+ // Check 3 (Site 3): new sibling as child of new operator.
1254
+ if (newSibling.type === "operator" &&
1255
+ newSibling.operator !== "not") {
1256
+ if (this.grammarConfig.autoNormalize) {
1257
+ siblingNeedsFormulaBuffer = true;
1258
+ }
1259
+ else {
1260
+ throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1261
+ }
1262
+ }
1263
+ }
1264
+ // Save the existing node's slot (the operator will inherit it).
1265
+ const anchorParentId = existingNode.parentId;
1266
+ const anchorPosition = existingNode.position;
1267
+ // Determine child positions (midpoint-spaced for future bisection).
1268
+ const existingPosition = leftNodeId !== undefined
1269
+ ? this.positionConfig.initial
1270
+ : midpoint(this.positionConfig.initial, this.positionConfig.max);
1271
+ const siblingPosition = leftNodeId !== undefined
1272
+ ? midpoint(this.positionConfig.initial, this.positionConfig.max)
1273
+ : this.positionConfig.initial;
1274
+ // Reparent existing node under operator.
1275
+ this.reparent(existingNodeId, operator.id, existingPosition);
1276
+ // Store new sibling under operator.
1277
+ const storedSibling = this.attachChecksum({
1278
+ ...newSibling,
1279
+ parentId: operator.id,
1280
+ position: siblingPosition,
1281
+ });
1282
+ this.expressions.set(newSibling.id, storedSibling);
1283
+ this.collector?.addedExpression({
1284
+ ...storedSibling,
1285
+ });
1286
+ getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
1287
+ getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(siblingPosition);
1288
+ // Determine the operator's slot. If a parent formula buffer is needed,
1289
+ // the formula takes the anchor slot and the operator goes under it.
1290
+ let operatorParentId = anchorParentId;
1291
+ let operatorPosition = anchorPosition;
1292
+ if (needsParentFormulaBuffer) {
1293
+ const formulaId = this.registerFormulaBuffer(operator, anchorParentId, anchorPosition);
1294
+ operatorParentId = formulaId;
1295
+ operatorPosition = 0;
1296
+ }
1297
+ // Store operator in its slot.
1298
+ const storedOperator = this.attachChecksum({
1299
+ ...operator,
1300
+ parentId: operatorParentId,
1301
+ position: operatorPosition,
1302
+ });
1303
+ this.expressions.set(operator.id, storedOperator);
1304
+ this.collector?.addedExpression({
1305
+ ...storedOperator,
1306
+ });
1307
+ getOrCreate(this.childExpressionIdsByParentId, operatorParentId, () => new Set()).add(operator.id);
1308
+ getOrCreate(this.childPositionsByParentId, operatorParentId, () => new Set()).add(operatorPosition);
1309
+ // Site 2: auto-insert formula buffer between operator and existing node.
1310
+ if (existingNodeNeedsFormulaBuffer) {
1311
+ const existingChild = this.expressions.get(existingNodeId);
1312
+ const childPosition = existingChild.position;
1313
+ const formulaId = this.generateId();
1314
+ // Reparent existing node under formula first (frees position in operator's tracking).
1315
+ // registerFormulaBuffer then occupies the freed position.
1316
+ this.reparent(existingNodeId, formulaId, 0);
1317
+ this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
1318
+ }
1319
+ // Site 3: auto-insert formula buffer between operator and new sibling.
1320
+ if (siblingNeedsFormulaBuffer) {
1321
+ const siblingChild = this.expressions.get(newSibling.id);
1322
+ const childPosition = siblingChild.position;
1323
+ const formulaId = this.generateId();
1324
+ // Reparent sibling under formula first (frees position in operator's tracking).
1325
+ // registerFormulaBuffer then occupies the freed position.
1326
+ this.reparent(newSibling.id, formulaId, 0);
1327
+ this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
1328
+ }
1329
+ // Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
1330
+ // reparent() already marks the existing node dirty; mark the operator and sibling as well.
1331
+ this.markExpressionDirty(newSibling.id);
1332
+ this.markExpressionDirty(operator.id);
1333
+ }
1334
+ /**
1335
+ * Reparents an expression to a new parent at a given position.
1336
+ */
1337
+ reparentExpression(expressionId, newParentId, newPosition) {
1338
+ const expression = this.expressions.get(expressionId);
1339
+ if (!expression) {
1340
+ throw new Error(`Expression "${expressionId}" does not exist.`);
1341
+ }
1342
+ this.reparent(expressionId, newParentId, newPosition);
1343
+ }
1344
+ /**
1345
+ * Deletes a single expression that has no children.
1346
+ * Does NOT trigger operator collapse. Caller must ensure children
1347
+ * have been reparented away first.
1348
+ */
1349
+ deleteExpression(expressionId) {
1350
+ const expression = this.expressions.get(expressionId);
1351
+ if (!expression)
1352
+ return undefined;
1353
+ const children = this.getChildExpressions(expressionId);
1354
+ if (children.length > 0) {
1355
+ throw new Error(`Cannot delete expression "${expressionId}" — it still has ${children.length} children. Reparent them first.`);
1356
+ }
1357
+ this.detachExpression(expressionId, expression);
1358
+ // Notify collector
1359
+ this.collector?.removedExpression({
1360
+ ...expression,
1361
+ });
1362
+ // Clean up dirty set and mark parent dirty
1363
+ this.dirtyExpressionIds.delete(expressionId);
1364
+ if (expression.parentId !== null) {
1365
+ this.markExpressionDirty(expression.parentId);
1366
+ }
1367
+ return expression;
1368
+ }
1369
+ /**
1370
+ * Changes the operator type of an operator expression without the swap
1371
+ * restriction enforced by {@link updateExpression}. Only validates that
1372
+ * the target expression is an operator, the new operator is not `"not"`,
1373
+ * and root-only constraints are satisfied.
1374
+ */
1375
+ changeOperatorType(expressionId, newOperator) {
1376
+ const expression = this.expressions.get(expressionId);
1377
+ if (!expression) {
1378
+ throw new Error(`Expression "${expressionId}" does not exist.`);
1379
+ }
1380
+ if (expression.type !== "operator") {
1381
+ throw new Error(`Expression "${expressionId}" is not an operator (type: "${expression.type}").`);
1382
+ }
1383
+ if (newOperator === "not") {
1384
+ throw new Error(`Cannot change operator to "not". Use toggleNegation instead.`);
1385
+ }
1386
+ // Root-only: implies/iff must be at root
1387
+ if ((newOperator === "implies" || newOperator === "iff") &&
1388
+ expression.parentId !== null) {
1389
+ throw new Error(`Operator "${newOperator}" must be a root expression (parentId must be null).`);
1390
+ }
1391
+ const updated = this.attachChecksum({
1392
+ ...expression,
1393
+ operator: newOperator,
1394
+ });
1395
+ this.expressions.set(expressionId, updated);
1396
+ this.collector?.modifiedExpression({
1397
+ ...updated,
1398
+ });
1399
+ this.markExpressionDirty(expressionId);
1400
+ return updated;
1401
+ }
1402
+ /**
1403
+ * Loads expressions in BFS order, respecting the current grammar config.
1404
+ * Used by restoration paths (fromData, rollback) that load existing data.
1405
+ */
1406
+ loadExpressions(expressions) {
1407
+ this.loadInitialExpressions(expressions);
1408
+ }
1409
+ /**
1410
+ * Performs a comprehensive validation sweep on all managed expressions.
1411
+ *
1412
+ * Collects ALL violations rather than failing on the first one. Checks:
1413
+ * schema validity, duplicate IDs, self-referential parents, parent
1414
+ * existence, parent container type, root-only operators, formula-between-
1415
+ * operators (when enabled), child limits, position uniqueness, and
1416
+ * checksum integrity.
1417
+ */
1418
+ validate() {
1419
+ const violations = [];
1420
+ const seenIds = new Set();
1421
+ // ── 1. Save pre-flush checksums for later comparison ──
1422
+ const preFlushChecksums = new Map();
1423
+ for (const [id, expr] of this.expressions) {
1424
+ if (expr.checksum != null) {
1425
+ preFlushChecksums.set(id, {
1426
+ checksum: expr.checksum,
1427
+ descendantChecksum: expr.descendantChecksum,
1428
+ combinedChecksum: expr.combinedChecksum,
1429
+ });
1430
+ }
1431
+ }
1432
+ // ── 2. Flush checksums to get fresh values ──
1433
+ // Mark all expressions dirty so flush recomputes everything
1434
+ for (const id of this.expressions.keys()) {
1435
+ this.dirtyExpressionIds.add(id);
1436
+ }
1437
+ this.flushExpressionChecksums();
1438
+ // ── 3. Per-expression checks ──
1439
+ // Build a sibling-position map for position uniqueness checks
1440
+ const positionsByParent = new Map();
1441
+ for (const [id, expr] of this.expressions) {
1442
+ // 3a. Schema check
1443
+ if (!Value.Check(CorePropositionalExpressionSchema, expr)) {
1444
+ violations.push({
1445
+ code: EXPR_SCHEMA_INVALID,
1446
+ message: `Expression "${id}" does not conform to CorePropositionalExpressionSchema.`,
1447
+ entityType: "expression",
1448
+ entityId: id,
1449
+ });
1450
+ }
1451
+ // 3b. Duplicate ID
1452
+ if (seenIds.has(id)) {
1453
+ violations.push({
1454
+ code: EXPR_DUPLICATE_ID,
1455
+ message: `Duplicate expression ID "${id}".`,
1456
+ entityType: "expression",
1457
+ entityId: id,
1458
+ });
1459
+ }
1460
+ seenIds.add(id);
1461
+ // 3c. Self-referential parent
1462
+ if (expr.parentId === id) {
1463
+ violations.push({
1464
+ code: EXPR_SELF_REFERENTIAL_PARENT,
1465
+ message: `Expression "${id}" references itself as parent.`,
1466
+ entityType: "expression",
1467
+ entityId: id,
1468
+ });
1469
+ }
1470
+ // 3d. Parent existence
1471
+ if (expr.parentId !== null &&
1472
+ !this.expressions.has(expr.parentId)) {
1473
+ violations.push({
1474
+ code: EXPR_PARENT_NOT_FOUND,
1475
+ message: `Expression "${id}" references non-existent parent "${expr.parentId}".`,
1476
+ entityType: "expression",
1477
+ entityId: id,
1478
+ });
1479
+ }
1480
+ // 3e. Parent is container (operator or formula)
1481
+ if (expr.parentId !== null && this.expressions.has(expr.parentId)) {
1482
+ const parent = this.expressions.get(expr.parentId);
1483
+ if (parent.type !== "operator" && parent.type !== "formula") {
1484
+ violations.push({
1485
+ code: EXPR_PARENT_NOT_CONTAINER,
1486
+ message: `Expression "${id}" has parent "${expr.parentId}" of type "${parent.type}" (expected operator or formula).`,
1487
+ entityType: "expression",
1488
+ entityId: id,
1489
+ });
1490
+ }
1491
+ }
1492
+ // 3f. Root-only: implies/iff must have parentId === null
1493
+ if (expr.type === "operator" &&
1494
+ (expr.operator === "implies" || expr.operator === "iff") &&
1495
+ expr.parentId !== null) {
1496
+ violations.push({
1497
+ code: EXPR_ROOT_ONLY_VIOLATED,
1498
+ message: `Root-only operator "${expr.operator}" expression "${id}" has non-null parentId "${expr.parentId}".`,
1499
+ entityType: "expression",
1500
+ entityId: id,
1501
+ });
1502
+ }
1503
+ // 3g. Formula-between-operators
1504
+ if (this.grammarConfig.enforceFormulaBetweenOperators &&
1505
+ expr.parentId !== null &&
1506
+ expr.type === "operator" &&
1507
+ expr.operator !== "not") {
1508
+ const parent = this.expressions.get(expr.parentId);
1509
+ if (parent && parent.type === "operator") {
1510
+ violations.push({
1511
+ code: EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED,
1512
+ message: `Non-not operator "${expr.operator}" expression "${id}" is a direct child of operator "${expr.parentId}".`,
1513
+ entityType: "expression",
1514
+ entityId: id,
1515
+ });
1516
+ }
1517
+ }
1518
+ // Collect positions for uniqueness check
1519
+ const parentKey = expr.parentId;
1520
+ let parentPositions = positionsByParent.get(parentKey);
1521
+ if (!parentPositions) {
1522
+ parentPositions = new Map();
1523
+ positionsByParent.set(parentKey, parentPositions);
1524
+ }
1525
+ const idsAtPosition = parentPositions.get(expr.position);
1526
+ if (idsAtPosition) {
1527
+ idsAtPosition.push(id);
1528
+ }
1529
+ else {
1530
+ parentPositions.set(expr.position, [id]);
1531
+ }
1532
+ // 3j. Checksum comparison
1533
+ const pre = preFlushChecksums.get(id);
1534
+ if (pre) {
1535
+ const fresh = this.expressions.get(id);
1536
+ if (pre.checksum !== fresh.checksum ||
1537
+ pre.descendantChecksum !== fresh.descendantChecksum ||
1538
+ pre.combinedChecksum !== fresh.combinedChecksum) {
1539
+ violations.push({
1540
+ code: EXPR_CHECKSUM_MISMATCH,
1541
+ message: `Expression "${id}" checksum mismatch: stored does not match recomputed.`,
1542
+ entityType: "expression",
1543
+ entityId: id,
1544
+ });
1545
+ }
1546
+ }
1547
+ }
1548
+ // ── 4. Child limit checks (not/formula: max 1 child) ──
1549
+ for (const [id, expr] of this.expressions) {
1550
+ if ((expr.type === "operator" && expr.operator === "not") ||
1551
+ expr.type === "formula") {
1552
+ const childIds = this.childExpressionIdsByParentId.get(id);
1553
+ const childCount = childIds?.size ?? 0;
1554
+ if (childCount > 1) {
1555
+ const label = expr.type === "formula" ? "Formula" : `Operator "not"`;
1556
+ violations.push({
1557
+ code: EXPR_CHILD_LIMIT_EXCEEDED,
1558
+ message: `${label} expression "${id}" has ${childCount} children (max 1).`,
1559
+ entityType: "expression",
1560
+ entityId: id,
1561
+ });
1562
+ }
1563
+ }
1564
+ }
1565
+ // ── 5. Position uniqueness ──
1566
+ for (const [, posMap] of positionsByParent) {
1567
+ for (const [position, ids] of posMap) {
1568
+ if (ids.length > 1) {
1569
+ for (const id of ids) {
1570
+ violations.push({
1571
+ code: EXPR_POSITION_DUPLICATE,
1572
+ message: `Position ${position} is shared by expressions [${ids.join(", ")}].`,
1573
+ entityType: "expression",
1574
+ entityId: id,
1575
+ });
1576
+ }
1577
+ }
1578
+ }
1579
+ }
1580
+ return {
1581
+ ok: violations.length === 0,
1582
+ violations,
1583
+ };
1584
+ }
1585
+ /** Returns a serializable snapshot of the current state. */
1586
+ snapshot() {
1587
+ return {
1588
+ expressions: this.toArray(),
1589
+ config: this.config
1590
+ ? {
1591
+ ...this.config,
1592
+ checksumConfig: serializeChecksumConfig(this.config.checksumConfig),
1593
+ }
1594
+ : this.config,
1595
+ };
1596
+ }
1597
+ /** Creates a new ExpressionManager from a previously captured snapshot. */
1598
+ static fromSnapshot(snapshot, grammarConfig, generateId) {
1599
+ // Normalize checksumConfig in case the snapshot went through a JSON
1600
+ // round-trip that converted Sets to arrays or empty objects.
1601
+ const normalizedChecksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
1602
+ const normalizedConfig = snapshot.config
1603
+ ? {
1604
+ ...snapshot.config,
1605
+ checksumConfig: normalizedChecksumConfig,
1606
+ }
1607
+ : undefined;
1608
+ // During loading: use explicit grammarConfig, falling back to snapshot's config
1609
+ const loadingConfig = {
1610
+ ...normalizedConfig,
1611
+ grammarConfig: grammarConfig ?? normalizedConfig?.grammarConfig,
1612
+ generateId: generateId ?? normalizedConfig?.generateId,
1613
+ };
1614
+ const em = new ExpressionManager(loadingConfig);
1615
+ em.loadInitialExpressions(snapshot.expressions);
1616
+ // After loading: restore the normalized config for ongoing mutations
1617
+ // (generateId is preserved via the em.generateId field set in constructor)
1618
+ em.config = normalizedConfig;
1619
+ return em;
1620
+ }
1621
+ }
1622
+ //# sourceMappingURL=expression-manager.js.map