@aryacode/dsh-engineering 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/cordis.patch.yml +122 -0
  5. package/locale/en.json +6 -0
  6. package/locale/zh.json +6 -0
  7. package/package.json +77 -3
  8. package/vendor/ftview/README.md +55 -0
  9. package/vendor/ftview/SKILL.md +74 -0
  10. package/vendor/ftview/lib/checks.d.ts +27 -0
  11. package/vendor/ftview/lib/checks.js +39 -0
  12. package/vendor/ftview/lib/core.d.ts +208 -0
  13. package/vendor/ftview/lib/core.js +160 -0
  14. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  15. package/vendor/ftview/lib/edit/drafts.js +84 -0
  16. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  17. package/vendor/ftview/lib/edit/operations.js +126 -0
  18. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  19. package/vendor/ftview/lib/edit/prepare.js +133 -0
  20. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  21. package/vendor/ftview/lib/edit/tools.js +198 -0
  22. package/vendor/ftview/lib/edit.d.ts +30 -0
  23. package/vendor/ftview/lib/edit.js +38 -0
  24. package/vendor/ftview/lib/index.d.ts +31 -0
  25. package/vendor/ftview/lib/index.js +36 -0
  26. package/vendor/ftview/lib/model.d.ts +80 -0
  27. package/vendor/ftview/lib/model.js +267 -0
  28. package/vendor/ftview/lib/session.d.ts +48 -0
  29. package/vendor/ftview/lib/session.js +98 -0
  30. package/vendor/ftview/lib/skill.d.ts +29 -0
  31. package/vendor/ftview/lib/skill.js +43 -0
  32. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  33. package/vendor/ftview/lib/studio/discover.js +87 -0
  34. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  35. package/vendor/ftview/lib/studio/preflight.js +64 -0
  36. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  37. package/vendor/ftview/lib/studio/tools.js +83 -0
  38. package/vendor/ftview/lib/studio.d.ts +30 -0
  39. package/vendor/ftview/lib/studio.js +37 -0
  40. package/vendor/ftview/lib/tools.d.ts +14 -0
  41. package/vendor/ftview/lib/tools.js +202 -0
  42. package/vendor/l5x/README.md +62 -0
  43. package/vendor/l5x/SKILL.md +81 -0
  44. package/vendor/l5x/lib/approvals.d.ts +50 -0
  45. package/vendor/l5x/lib/approvals.js +93 -0
  46. package/vendor/l5x/lib/client.js +412 -0
  47. package/vendor/l5x/lib/client.js.map +1 -0
  48. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  49. package/vendor/l5x/lib/core/errors.js +48 -0
  50. package/vendor/l5x/lib/core/params.d.ts +49 -0
  51. package/vendor/l5x/lib/core/params.js +39 -0
  52. package/vendor/l5x/lib/core/result.d.ts +46 -0
  53. package/vendor/l5x/lib/core/result.js +21 -0
  54. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  55. package/vendor/l5x/lib/core/tool.js +19 -0
  56. package/vendor/l5x/lib/drafts.d.ts +66 -0
  57. package/vendor/l5x/lib/drafts.js +98 -0
  58. package/vendor/l5x/lib/editing.d.ts +30 -0
  59. package/vendor/l5x/lib/editing.js +42 -0
  60. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  61. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  62. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  63. package/vendor/l5x/lib/engine/analysis.js +258 -0
  64. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  65. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  66. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  67. package/vendor/l5x/lib/engine/change-set.js +446 -0
  68. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  69. package/vendor/l5x/lib/engine/configuration.js +42 -0
  70. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  71. package/vendor/l5x/lib/engine/details.js +173 -0
  72. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  73. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  74. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  75. package/vendor/l5x/lib/engine/download-review.js +63 -0
  76. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  77. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  78. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  79. package/vendor/l5x/lib/engine/observations.js +67 -0
  80. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  81. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  82. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  83. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  84. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  85. package/vendor/l5x/lib/engine/review.js +123 -0
  86. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  87. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  88. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  89. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  90. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  91. package/vendor/l5x/lib/engine/usage.js +92 -0
  92. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  93. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  94. package/vendor/l5x/lib/fs.d.ts +17 -0
  95. package/vendor/l5x/lib/fs.js +36 -0
  96. package/vendor/l5x/lib/index.d.ts +36 -0
  97. package/vendor/l5x/lib/index.js +42 -0
  98. package/vendor/l5x/lib/live.d.ts +48 -0
  99. package/vendor/l5x/lib/live.js +48 -0
  100. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  101. package/vendor/l5x/lib/model/aois.js +112 -0
  102. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  103. package/vendor/l5x/lib/model/errors.js +18 -0
  104. package/vendor/l5x/lib/model/index.d.ts +20 -0
  105. package/vendor/l5x/lib/model/index.js +19 -0
  106. package/vendor/l5x/lib/model/module.d.ts +37 -0
  107. package/vendor/l5x/lib/model/module.js +54 -0
  108. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  109. package/vendor/l5x/lib/model/modules.js +55 -0
  110. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  111. package/vendor/l5x/lib/model/parse.js +116 -0
  112. package/vendor/l5x/lib/model/project.d.ts +15 -0
  113. package/vendor/l5x/lib/model/project.js +31 -0
  114. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  115. package/vendor/l5x/lib/model/routines.js +183 -0
  116. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  117. package/vendor/l5x/lib/model/tags.js +119 -0
  118. package/vendor/l5x/lib/model/types.d.ts +304 -0
  119. package/vendor/l5x/lib/model/types.js +12 -0
  120. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  121. package/vendor/l5x/lib/model/udts.js +81 -0
  122. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  123. package/vendor/l5x/lib/model/xml.js +291 -0
  124. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  125. package/vendor/l5x/lib/model/xref.js +154 -0
  126. package/vendor/l5x/lib/session.d.ts +119 -0
  127. package/vendor/l5x/lib/session.js +236 -0
  128. package/vendor/l5x/lib/skill.d.ts +30 -0
  129. package/vendor/l5x/lib/skill.js +44 -0
  130. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  131. package/vendor/l5x/lib/studio-state.js +79 -0
  132. package/vendor/l5x/lib/studio.d.ts +29 -0
  133. package/vendor/l5x/lib/studio.js +40 -0
  134. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  135. package/vendor/l5x/lib/tools/checks.js +140 -0
  136. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  137. package/vendor/l5x/lib/tools/common.js +84 -0
  138. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  139. package/vendor/l5x/lib/tools/controller.js +170 -0
  140. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  141. package/vendor/l5x/lib/tools/drafts.js +270 -0
  142. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  143. package/vendor/l5x/lib/tools/live.js +175 -0
  144. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  145. package/vendor/l5x/lib/tools/read.js +245 -0
  146. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  147. package/vendor/l5x/lib/tools/references.js +269 -0
  148. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  149. package/vendor/l5x/lib/tools/studio.js +193 -0
  150. package/vendor/l5x/poll-worker.mjs +77 -0
  151. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  152. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  153. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  154. package/vendor/mwi/README.md +60 -0
  155. package/vendor/mwi/SKILL.md +87 -0
  156. package/vendor/mwi/bridge/README.md +8 -0
  157. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  158. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  159. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  160. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  161. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  162. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  163. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  164. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  165. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  229. package/vendor/mwi/code/mw_code.py +1258 -0
  230. package/vendor/mwi/legacy/edit-session.js +50 -0
  231. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  232. package/vendor/mwi/legacy/native-code.js +83 -0
  233. package/vendor/mwi/legacy/native-groups.js +75 -0
  234. package/vendor/mwi/legacy/native-structure.js +143 -0
  235. package/vendor/mwi/legacy/native-variables.js +77 -0
  236. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  237. package/vendor/mwi/legacy/pou-package.js +66 -0
  238. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  239. package/vendor/mwi/lib/core/bridge.js +53 -0
  240. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  241. package/vendor/mwi/lib/core/code-engine.js +109 -0
  242. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  243. package/vendor/mwi/lib/core/errors.js +47 -0
  244. package/vendor/mwi/lib/core/params.d.ts +52 -0
  245. package/vendor/mwi/lib/core/params.js +46 -0
  246. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  247. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  248. package/vendor/mwi/lib/core/result.d.ts +45 -0
  249. package/vendor/mwi/lib/core/result.js +20 -0
  250. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  251. package/vendor/mwi/lib/core/tool.js +55 -0
  252. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  253. package/vendor/mwi/lib/edit/compare.js +70 -0
  254. package/vendor/mwi/lib/edit.d.ts +20 -0
  255. package/vendor/mwi/lib/edit.js +32 -0
  256. package/vendor/mwi/lib/index.d.ts +21 -0
  257. package/vendor/mwi/lib/index.js +33 -0
  258. package/vendor/mwi/lib/read/service.d.ts +117 -0
  259. package/vendor/mwi/lib/read/service.js +229 -0
  260. package/vendor/mwi/lib/read.d.ts +21 -0
  261. package/vendor/mwi/lib/read.js +33 -0
  262. package/vendor/mwi/lib/session/project.d.ts +34 -0
  263. package/vendor/mwi/lib/session/project.js +54 -0
  264. package/vendor/mwi/lib/session/service.d.ts +167 -0
  265. package/vendor/mwi/lib/session/service.js +566 -0
  266. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  267. package/vendor/mwi/lib/session/stage.js +187 -0
  268. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  269. package/vendor/mwi/lib/session/workspace.js +90 -0
  270. package/vendor/mwi/lib/skill.d.ts +36 -0
  271. package/vendor/mwi/lib/skill.js +56 -0
  272. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  273. package/vendor/mwi/lib/tools/edit.js +286 -0
  274. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  275. package/vendor/mwi/lib/tools/read.js +161 -0
  276. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  277. package/vendor/mwi/lib/tools/session.js +166 -0
  278. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  279. package/vendor/mwi/lib/tools/verify.js +85 -0
  280. package/vendor/mwi/lib/verify.d.ts +17 -0
  281. package/vendor/mwi/lib/verify.js +29 -0
@@ -0,0 +1,563 @@
1
+ """Parse the ``PROJECT.TRE`` project-tree stream.
2
+
3
+ MotionWorks keeps the project structure in the ``PROJECT.TRE`` stream of the root
4
+ ``src.st1``. It is the authority for which program instances run in which task,
5
+ and for the node identities a structural edit (creating or deleting a POU,
6
+ assigning a task) must maintain.
7
+
8
+ Layout, established from real trees::
9
+
10
+ line 0 version marker ("V51")
11
+ line 1 total node count
12
+ line 2 configuration count
13
+ line 3 the root node's params line
14
+ then per node:
15
+ <count> subtree node count
16
+ <id> <a> <children> <d> params
17
+ <name>
18
+ <path> optional; blank for container nodes
19
+ <state line(s)> tab-separated; may hold -1 and alnum handles
20
+ <GUID> optional
21
+ <marker> optional, e.g. "<3>"
22
+
23
+ **The child count is ``params[2]``, not ``params[1]``.** ``Project 0 0 56 0``
24
+ with header total 57 means 56 children plus the root itself, and
25
+ ``Logical POUs 4 1 28 0`` means 28 children -- 7 POUs of 4 records each.
26
+
27
+ Five record-shape details each caused a *silent* desynchronisation, which is the
28
+ worst failure mode because the parse still succeeds while producing wrong output.
29
+ All are handled here and covered by tests in ``tools/test_tree.py``:
30
+
31
+ 1. the child count is ``params[2]``;
32
+ 2. a container's state line can be almost all tabs, so testing the *stripped*
33
+ line loses the tabs and it reads as a bare number;
34
+ 3. state fields can be negative (``-1``);
35
+ 4. handles are mixed alphanumeric (``a449b235``) and up to 28 characters;
36
+ 5. a node can carry a bare handle line with no tabs, plus a marker such as
37
+ ``<3>``.
38
+
39
+ The crux is telling a node's state line from the next node's count line: both can
40
+ be a bare integer. They are distinguished by lookahead -- a genuine count line is
41
+ immediately followed by a params line.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import re
47
+ from dataclasses import dataclass, field
48
+ from pathlib import Path
49
+
50
+ from .cfb import CompoundFile
51
+
52
+ #: A params line: four integers, e.g. "0 0 56 0".
53
+ PARAMS_RE = re.compile(r"^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$")
54
+
55
+ #: GUID lines look like "3D84B45B C9CE 4C2A B1 F2 B7 AC FC B1 A9 94".
56
+ GUID_RE = re.compile(r"^[0-9A-Fa-f]{8}(\s+[0-9A-Fa-f]{4}){1,}(\s+[0-9A-Fa-f]{2})+$")
57
+
58
+ #: A trailing annotation such as "<3>" seen on some nodes.
59
+ TRAILING_MARKER_RE = re.compile(r"^<\d+>$")
60
+
61
+ #: A task node's path points at the resource task directory.
62
+ _TASK_PATH_RE = re.compile(
63
+ r"Configuration[\\/]R[\\/]Resource[\\/][^\\/\t]+$", re.IGNORECASE
64
+ )
65
+
66
+
67
+ class TreeParseError(ValueError):
68
+ """The tree did not match the expected shape."""
69
+
70
+
71
+ @dataclass
72
+ class TreeNode:
73
+ """One node in the project tree.
74
+
75
+ ``descendants`` is ``params[2]``: a subtree size used as an upper bound on the
76
+ child loop. It is deliberately not treated as an exact child count -- see the
77
+ note in :func:`parse_tree`.
78
+
79
+ ``start_line`` and ``end_line`` are the span of source lines this node owns,
80
+ from its own count/params line through everything up to (but excluding) the
81
+ next sibling. Recording the span is what makes a byte-exact re-emit possible:
82
+ the parser does not have to model every record kind, only prove it accounted
83
+ for all of them. That property is the prerequisite for writing the tree,
84
+ because a writer built on a lossy parse would silently corrupt a project.
85
+ """
86
+
87
+ node_id: int
88
+ descendants: int
89
+ params: tuple[int, int, int, int]
90
+ name: str
91
+ path: str
92
+ line: int
93
+ children: list["TreeNode"] = field(default_factory=list)
94
+ start_line: int = 0
95
+ end_line: int = 0
96
+
97
+ @property
98
+ def span(self) -> tuple[int, int]:
99
+ """Half-open ``(start, end)`` line range owned by this node."""
100
+ return (self.start_line, self.end_line)
101
+
102
+ def walk(self, depth: int = 0):
103
+ yield depth, self
104
+ for child in self.children:
105
+ yield from child.walk(depth + 1)
106
+
107
+ def find(self, predicate) -> list["TreeNode"]:
108
+ return [node for _, node in self.walk() if predicate(node)]
109
+
110
+
111
+ def _next_nonblank(lines: list[str], index: int) -> int | None:
112
+ look = index + 1
113
+ while look < len(lines) and not lines[look].strip():
114
+ look += 1
115
+ return look if look < len(lines) else None
116
+
117
+
118
+ def header_starts_node(lines: list[str], index: int) -> bool:
119
+ """True when ``lines[index]`` begins a new node's header block.
120
+
121
+ Two forms exist:
122
+
123
+ * a **params line** -- the root has no count line of its own, so its header
124
+ begins directly with one on line 3;
125
+ * a **bare integer followed by a params line** -- how every child begins.
126
+
127
+ The lookahead is essential. A node's state line can also be a bare integer
128
+ on its own (``'\\t\\t\\t\\t0\\t'`` strips to ``'0'``), and a params test alone
129
+ cannot tell the two apart. What separates them is that a real count line is
130
+ immediately followed by that node's params line, whereas a state line is
131
+ followed by a blank or another state line.
132
+ """
133
+ stripped = lines[index].strip()
134
+ if PARAMS_RE.match(stripped):
135
+ return True
136
+ if not stripped.isdigit():
137
+ return False
138
+ following = _next_nonblank(lines, index)
139
+ return following is not None and bool(PARAMS_RE.match(lines[following].strip()))
140
+
141
+
142
+ def is_state_line(line: str) -> bool:
143
+ """True for a node's tab-separated numeric state line.
144
+
145
+ These are mostly digits, but a field can be negative or an alphanumeric
146
+ handle, and a container's state line may be almost entirely tabs::
147
+
148
+ 0\\t0\\t0\\t0\\t0\\t0\\t0\\t0\\t6522df69
149
+ \\t\\t\\t\\t0\\t
150
+
151
+ Two easy mistakes both cause a silent desynchronisation: requiring all-digits
152
+ rejects the handle form, and testing the *stripped* line loses the tabs so an
153
+ all-tabs line looks like a bare number.
154
+ """
155
+ if "\t" not in line:
156
+ return False
157
+ fields = line.strip().split("\t")
158
+ if not all(
159
+ f == ""
160
+ or re.fullmatch(r"-?\d+", f) # numeric, may be -1
161
+ or re.fullmatch(r"[0-9A-Za-z]{1,64}", f) # handle, length varies
162
+ for f in fields
163
+ ):
164
+ return False
165
+ return any(re.fullmatch(r"-?\d+", f) for f in fields)
166
+
167
+
168
+ def parse_tree(text: str) -> tuple[list[TreeNode], list[str]]:
169
+ """Parse a ``PROJECT.TRE`` payload into top-level nodes.
170
+
171
+ Returns ``(roots, warnings)``. A node's declared child count delimits its
172
+ children. A shortfall is reported as a warning rather than raised, because a
173
+ partially readable tree is still useful -- but a shortfall is exactly the
174
+ signal that the walk went wrong, so it must not be swallowed.
175
+ """
176
+ warnings: list[str] = []
177
+ lines = text.replace("\r\n", "\n").split("\n")
178
+
179
+ if not lines or not lines[0].startswith("V"):
180
+ raise TreeParseError(f"unexpected tree header: {lines[0]!r}")
181
+ if len(lines) < 4:
182
+ raise TreeParseError("tree is too short to contain a root node")
183
+
184
+ index = 0
185
+
186
+ def skip_blanks() -> None:
187
+ nonlocal index
188
+ while index < len(lines) and not lines[index].strip():
189
+ index += 1
190
+
191
+ def read_node() -> TreeNode | None:
192
+ nonlocal index
193
+ skip_blanks()
194
+ if index >= len(lines):
195
+ return None
196
+ node_start = index
197
+
198
+ # A child begins with a bare count line; the root has no count line of
199
+ # its own, so accept a params line here too and validate it next.
200
+ head = lines[index].strip()
201
+ if not (head.isdigit() or PARAMS_RE.match(head)):
202
+ return None
203
+ if head.isdigit():
204
+ index += 1
205
+
206
+ skip_blanks()
207
+ if index >= len(lines):
208
+ return None
209
+ params_match = PARAMS_RE.match(lines[index].strip())
210
+ if params_match is None:
211
+ return None
212
+ params = tuple(int(group) for group in params_match.groups())
213
+ params_line = index
214
+ index += 1
215
+
216
+ skip_blanks()
217
+ if index >= len(lines):
218
+ return None
219
+ name = lines[index].split("\t")[0]
220
+ index += 1
221
+
222
+ # Optional path: present only when the line is neither blank, nor a state
223
+ # line, nor a GUID.
224
+ path = ""
225
+ if index < len(lines):
226
+ candidate = lines[index]
227
+ stripped = candidate.strip()
228
+ if (
229
+ stripped
230
+ and not GUID_RE.match(stripped)
231
+ and not is_state_line(candidate)
232
+ ):
233
+ path = candidate
234
+ index += 1
235
+
236
+ # Everything up to the next node's header belongs to this node: state
237
+ # line(s), the GUID, a bare handle, and a marker.
238
+ while index < len(lines):
239
+ stripped = lines[index].strip()
240
+ if not stripped:
241
+ look = index + 1
242
+ while look < len(lines) and not lines[look].strip():
243
+ look += 1
244
+ if look < len(lines) and header_starts_node(lines, look):
245
+ break
246
+ index += 1
247
+ continue
248
+ if header_starts_node(lines, index):
249
+ break
250
+ if TRAILING_MARKER_RE.match(stripped):
251
+ index += 1
252
+ continue
253
+ if GUID_RE.match(stripped) or is_state_line(lines[index]):
254
+ index += 1
255
+ continue
256
+ if re.fullmatch(r"[0-9A-Za-z]{2,64}", stripped):
257
+ # A bare handle with no tabs is still this node's data.
258
+ index += 1
259
+ continue
260
+ break
261
+
262
+ node = TreeNode(
263
+ node_id=params[0],
264
+ descendants=params[2],
265
+ params=params, # type: ignore[arg-type]
266
+ name=name,
267
+ path=path,
268
+ line=params_line,
269
+ start_line=node_start,
270
+ )
271
+ # `descendants` (params[2]) is the *subtree* size, used as an upper bound
272
+ # on the child loop. It is not an exact child count: `Tasks` declares 11
273
+ # where 7 children follow, and `Resource` declares 14 with 1. The loop is
274
+ # therefore self-terminating -- read_node returns None once the node's
275
+ # records are exhausted and the next sibling's header appears -- and a
276
+ # shortfall is normal rather than an error.
277
+ #
278
+ # Each child's span ends where the next child begins, so between them the
279
+ # children own every line of this node's subtree with no gaps and no
280
+ # overlap. That is what makes a byte-exact re-emit verifiable.
281
+ previous: TreeNode | None = None
282
+ for _ in range(node.descendants):
283
+ child_start = index
284
+ if previous is not None:
285
+ previous.end_line = child_start
286
+ child = read_node()
287
+ if child is None:
288
+ index = child_start
289
+ break
290
+ node.children.append(child)
291
+ previous = child
292
+ node.end_line = index
293
+ return node
294
+
295
+ # The root's params line is line 3. Skipping "until a numbered line" is
296
+ # wrong, because the root's own params line is numbered.
297
+ index = 3
298
+ roots: list[TreeNode] = []
299
+ root = read_node()
300
+ if root is None:
301
+ raise TreeParseError("could not read the root node")
302
+ roots.append(root)
303
+
304
+ while True:
305
+ node = read_node()
306
+ if node is None:
307
+ break
308
+ roots.append(node)
309
+ return roots, warnings
310
+
311
+
312
+ def load_tree(project_or_src: Path) -> tuple[list[TreeNode], str, list[str]]:
313
+ """Load and parse ``PROJECT.TRE`` from a project directory or root src.st1."""
314
+ path = Path(project_or_src)
315
+ if path.is_dir():
316
+ path = path / "src.st1"
317
+ cfb = CompoundFile(path)
318
+ if "PROJECT.TRE" not in cfb.stream_names():
319
+ raise TreeParseError(f"no PROJECT.TRE stream in {path}")
320
+ text = cfb.read_stream("PROJECT.TRE").decode("latin1")
321
+ roots, warnings = parse_tree(text)
322
+ return roots, text, warnings
323
+
324
+
325
+ def _walk_all(roots: list[TreeNode]):
326
+ for root in roots:
327
+ yield from root.walk()
328
+
329
+
330
+ def task_assignments(roots: list[TreeNode]) -> dict[str, list[str]]:
331
+ """Map task name -> assigned program instance names.
332
+
333
+ A task node's path points at the resource task directory **and its final path
334
+ component equals its name** -- ``C\\Configuration\\R\\Resource\\FastTsk`` for
335
+ the node called ``FastTsk``. Matching the directory pattern alone also
336
+ catches ``Global_Variables`` and ``IO_Configuration``, whose paths live in the
337
+ same directory but carry a file extension, and program-instance nodes carry
338
+ the task type in later fields rather than the path.
339
+ """
340
+ assignments: dict[str, list[str]] = {}
341
+ for _, node in _walk_all(roots):
342
+ record = node.path.split("\t")[0].replace("/", "\\")
343
+ if not _TASK_PATH_RE.search(record):
344
+ continue
345
+ if "\t" not in node.path:
346
+ continue
347
+ if record.split("\\")[-1] != node.name:
348
+ continue
349
+ assignments[node.name] = [child.name for child in node.children]
350
+ return assignments
351
+
352
+
353
+ def program_instances(roots: list[TreeNode]) -> list[tuple[str, str]]:
354
+ """Return ``(task_name, program_instance_name)`` pairs."""
355
+ pairs: list[tuple[str, str]] = []
356
+ for task, programs in task_assignments(roots).items():
357
+ for program in programs:
358
+ pairs.append((task, program))
359
+ return pairs
360
+
361
+
362
+ @dataclass
363
+ class TreeDocument:
364
+ """A parsed tree together with everything needed to re-emit it exactly.
365
+
366
+ Keeping the original text means a re-emit cannot lose a record the parser
367
+ does not model. That is the whole point: it turns "the parse looks right"
368
+ into "the parse provably accounts for every byte", which is the prerequisite
369
+ for ever writing the tree.
370
+
371
+ ``text`` is the verbatim input. ``lines`` is the LF-normalised view used for
372
+ line indexing, and is *not* what gets emitted -- joining it back would rewrite
373
+ every CRLF, which is exactly the kind of silent change this design exists to
374
+ rule out. A future edit will splice replacement lines into ``text``.
375
+ """
376
+
377
+ text: str
378
+ lines: list[str]
379
+ roots: list[TreeNode]
380
+ warnings: list[str] = field(default_factory=list)
381
+
382
+ @property
383
+ def ending(self) -> str:
384
+ return "\r\n" if "\r\n" in self.text else "\n"
385
+
386
+ def emit(self) -> str:
387
+ """Re-emit the original text verbatim.
388
+
389
+ Byte-identical by construction. Before any write exists this is the
390
+ safest possible emitter: it cannot invent a change. Edits replace the
391
+ affected line ranges in ``text`` rather than re-serialising records.
392
+ """
393
+ return self.text
394
+
395
+ def lines_for(self, span: tuple[int, int]) -> list[str]:
396
+ """The source lines a span covers, without line endings."""
397
+ return self.lines[span[0]:span[1]]
398
+
399
+ def splice(self, edits: list[tuple[int, int, list[str]]]) -> str:
400
+ """Apply several line-level edits at once and re-emit.
401
+
402
+ ``edits`` are ``(start, end, replacement_lines)``; ``start == end``
403
+ inserts. Edits are applied right-to-left so earlier line numbers stay
404
+ valid, which means a caller can compute every span from one parse and
405
+ then commit them together -- important when one edit shifts the lines of
406
+ another.
407
+ """
408
+ lines = list(self.lines)
409
+ # Right-to-left, so edits at lower line numbers keep their coordinates.
410
+ # Insertions sort before replacements at the same start, and the tuple
411
+ # carries a type tag because a plain `start` key collides when one edit
412
+ # inserts at a line another edit rewrites.
413
+ ordered = sorted(edits, key=lambda e: (e[0], 0 if e[0] == e[1] else 1),
414
+ reverse=True)
415
+ for start, end, replacement in ordered:
416
+ lines[start:end] = list(replacement)
417
+ return self.ending.join(lines)
418
+
419
+ def walk_with_ancestors(
420
+ self,
421
+ ) -> list[tuple[TreeNode, list[TreeNode]]]:
422
+ """Every node paired with its ancestor chain, root first.
423
+
424
+ Mutating a node's child count changes every ancestor's subtree size, so a
425
+ caller needs the chain, not just the node.
426
+ """
427
+ out: list[tuple[TreeNode, list[TreeNode]]] = []
428
+
429
+ def visit(node: TreeNode, ancestors: list[TreeNode]) -> None:
430
+ out.append((node, ancestors))
431
+ for child in node.children:
432
+ visit(child, ancestors + [node])
433
+
434
+ for root in self.roots:
435
+ visit(root, [])
436
+ return out
437
+
438
+ def replace_span(self, span: tuple[int, int], replacement: list[str]) -> str:
439
+ """Return the text with one span's lines swapped out.
440
+
441
+ Splices at line granularity into the original text, so every line outside
442
+ the span survives byte-for-byte and the file's line ending is preserved.
443
+ That is the property a structural edit needs: change a node's records
444
+ without touching the rest of the project tree.
445
+
446
+ Callers are responsible for keeping the header counts consistent -- see
447
+ :func:`set_header_total`.
448
+ """
449
+ start, end = span
450
+ before = self.lines[:start]
451
+ after = self.lines[end:]
452
+ return self.ending.join(before + list(replacement) + after)
453
+
454
+ def set_header_total(self, total: int) -> str:
455
+ """Return the text with line 1's node count changed.
456
+
457
+ Any structural edit changes the node total, and MotionWorks keeps it on
458
+ its own line, so this is the one header field that must move with it.
459
+ """
460
+ if len(self.lines) < 2:
461
+ raise TreeParseError("tree has no total-count line")
462
+ lines = list(self.lines)
463
+ lines[1] = str(total)
464
+ return self.ending.join(lines)
465
+
466
+ def lineage(self) -> list[TreeNode]:
467
+ """Nodes in source order, including interior nodes.
468
+
469
+ A parent's span necessarily contains its children's, so spans are nested
470
+ rather than disjoint. :meth:`record_spans` returns the disjoint set that
471
+ actually partitions the file.
472
+ """
473
+ out: list[TreeNode] = []
474
+ for root in self.roots:
475
+ for _, node in root.walk():
476
+ out.append(node)
477
+ return out
478
+
479
+ def record_spans(self) -> list[tuple[int, int]]:
480
+ """Disjoint spans covering every record line, in source order.
481
+
482
+ A node's own records are its span minus its children's spans, so
483
+ subtracting the children yields one contiguous run per node. Those runs
484
+ are disjoint by construction, and a correct parse leaves no line unowned.
485
+ Used for both the coverage check and, later, targeted edits.
486
+ """
487
+ spans: list[tuple[int, int]] = []
488
+
489
+ def visit(node: TreeNode) -> None:
490
+ cursor = node.start_line
491
+ for child in node.children:
492
+ if child.start_line > cursor:
493
+ spans.append((cursor, child.start_line))
494
+ visit(child)
495
+ cursor = max(cursor, child.end_line)
496
+ if node.end_line > cursor:
497
+ spans.append((cursor, node.end_line))
498
+
499
+ for root in self.roots:
500
+ visit(root)
501
+ return sorted(spans)
502
+
503
+ def coverage(self) -> tuple[list[int], list[int]]:
504
+ """Record line indices covered, and any owned more than once.
505
+
506
+ Node spans are absolute indices into the same ``lines`` list held here,
507
+ so the comparison is direct. A correct parse assigns every line after the
508
+ three header lines to exactly one node.
509
+ """
510
+ covered: list[int] = []
511
+ for start, end in self.record_spans():
512
+ covered.extend(range(start, end))
513
+ counts: dict[int, int] = {}
514
+ for index in covered:
515
+ counts[index] = counts.get(index, 0) + 1
516
+ duplicates = sorted(index for index, count in counts.items() if count > 1)
517
+ return sorted(counts), duplicates
518
+
519
+
520
+ def parse_document(text: str) -> TreeDocument:
521
+ """Parse a tree while retaining the source needed for an exact re-emit."""
522
+ roots, warnings = parse_tree(text)
523
+ return TreeDocument(
524
+ text=text,
525
+ lines=text.replace("\r\n", "\n").split("\n"),
526
+ roots=roots,
527
+ warnings=warnings,
528
+ )
529
+
530
+
531
+ def round_trip(text: str) -> tuple[bool, str, list[str]]:
532
+ """Check that parsing then re-emitting reproduces ``text`` exactly.
533
+
534
+ Returns ``(matches, emitted, notes)``. This is the gate on writing the tree:
535
+ a parse that cannot reproduce the input byte-for-byte is not trustworthy
536
+ enough to modify.
537
+ """
538
+ document = parse_document(text)
539
+ emitted = document.emit()
540
+
541
+ notes: list[str] = []
542
+ if emitted != text:
543
+ notes.append("re-emitted text differs from the input")
544
+ original_lines = text.replace("\r\n", "\n").split("\n")
545
+ emitted_lines = emitted.replace("\r\n", "\n").split("\n")
546
+ for index in range(max(len(original_lines), len(emitted_lines))):
547
+ before = original_lines[index] if index < len(original_lines) else "<eof>"
548
+ after = emitted_lines[index] if index < len(emitted_lines) else "<eof>"
549
+ if before != after:
550
+ notes.append(
551
+ f"first difference at line {index}: {before!r} -> {after!r}"
552
+ )
553
+ break
554
+
555
+ covered, duplicates = document.coverage()
556
+ expected = set(range(3, len(document.lines)))
557
+ missing = sorted(expected - set(covered))
558
+ if missing:
559
+ notes.append(f"{len(missing)} line(s) not owned by any node: {missing[:8]}")
560
+ if duplicates:
561
+ notes.append(f"line(s) owned by more than one node: {duplicates[:8]}")
562
+
563
+ return (emitted == text, emitted, notes)