@aryacode/dsh-engineering 0.0.0-stage → 0.4.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 (284) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/LICENSE +21 -0
  3. package/README.md +230 -2
  4. package/cordis.patch.yml +123 -0
  5. package/lib/client.js +3448 -0
  6. package/lib/index.d.ts +2 -0
  7. package/lib/index.js +9 -0
  8. package/locale/en.json +6 -0
  9. package/locale/zh.json +6 -0
  10. package/package.json +94 -3
  11. package/vendor/ftview/README.md +55 -0
  12. package/vendor/ftview/SKILL.md +74 -0
  13. package/vendor/ftview/lib/checks.d.ts +27 -0
  14. package/vendor/ftview/lib/checks.js +39 -0
  15. package/vendor/ftview/lib/core.d.ts +208 -0
  16. package/vendor/ftview/lib/core.js +160 -0
  17. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  18. package/vendor/ftview/lib/edit/drafts.js +84 -0
  19. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  20. package/vendor/ftview/lib/edit/operations.js +126 -0
  21. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  22. package/vendor/ftview/lib/edit/prepare.js +133 -0
  23. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  24. package/vendor/ftview/lib/edit/tools.js +198 -0
  25. package/vendor/ftview/lib/edit.d.ts +30 -0
  26. package/vendor/ftview/lib/edit.js +38 -0
  27. package/vendor/ftview/lib/index.d.ts +31 -0
  28. package/vendor/ftview/lib/index.js +36 -0
  29. package/vendor/ftview/lib/model.d.ts +80 -0
  30. package/vendor/ftview/lib/model.js +267 -0
  31. package/vendor/ftview/lib/session.d.ts +48 -0
  32. package/vendor/ftview/lib/session.js +98 -0
  33. package/vendor/ftview/lib/skill.d.ts +29 -0
  34. package/vendor/ftview/lib/skill.js +43 -0
  35. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  36. package/vendor/ftview/lib/studio/discover.js +87 -0
  37. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  38. package/vendor/ftview/lib/studio/preflight.js +64 -0
  39. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  40. package/vendor/ftview/lib/studio/tools.js +83 -0
  41. package/vendor/ftview/lib/studio.d.ts +30 -0
  42. package/vendor/ftview/lib/studio.js +37 -0
  43. package/vendor/ftview/lib/tools.d.ts +14 -0
  44. package/vendor/ftview/lib/tools.js +202 -0
  45. package/vendor/l5x/README.md +62 -0
  46. package/vendor/l5x/SKILL.md +81 -0
  47. package/vendor/l5x/lib/approvals.d.ts +50 -0
  48. package/vendor/l5x/lib/approvals.js +93 -0
  49. package/vendor/l5x/lib/client.js +412 -0
  50. package/vendor/l5x/lib/client.js.map +1 -0
  51. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  52. package/vendor/l5x/lib/core/errors.js +48 -0
  53. package/vendor/l5x/lib/core/params.d.ts +49 -0
  54. package/vendor/l5x/lib/core/params.js +39 -0
  55. package/vendor/l5x/lib/core/result.d.ts +46 -0
  56. package/vendor/l5x/lib/core/result.js +21 -0
  57. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  58. package/vendor/l5x/lib/core/tool.js +19 -0
  59. package/vendor/l5x/lib/drafts.d.ts +66 -0
  60. package/vendor/l5x/lib/drafts.js +98 -0
  61. package/vendor/l5x/lib/editing.d.ts +30 -0
  62. package/vendor/l5x/lib/editing.js +42 -0
  63. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  64. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  65. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  66. package/vendor/l5x/lib/engine/analysis.js +258 -0
  67. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  68. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  69. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  70. package/vendor/l5x/lib/engine/change-set.js +446 -0
  71. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  72. package/vendor/l5x/lib/engine/configuration.js +42 -0
  73. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  74. package/vendor/l5x/lib/engine/details.js +173 -0
  75. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  76. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  77. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  78. package/vendor/l5x/lib/engine/download-review.js +63 -0
  79. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  80. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  81. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  82. package/vendor/l5x/lib/engine/observations.js +67 -0
  83. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  84. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  85. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  86. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  87. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  88. package/vendor/l5x/lib/engine/review.js +123 -0
  89. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  90. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  91. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  92. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  93. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  94. package/vendor/l5x/lib/engine/usage.js +92 -0
  95. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  96. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  97. package/vendor/l5x/lib/fs.d.ts +17 -0
  98. package/vendor/l5x/lib/fs.js +36 -0
  99. package/vendor/l5x/lib/index.d.ts +36 -0
  100. package/vendor/l5x/lib/index.js +42 -0
  101. package/vendor/l5x/lib/live.d.ts +48 -0
  102. package/vendor/l5x/lib/live.js +48 -0
  103. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  104. package/vendor/l5x/lib/model/aois.js +112 -0
  105. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  106. package/vendor/l5x/lib/model/errors.js +18 -0
  107. package/vendor/l5x/lib/model/index.d.ts +20 -0
  108. package/vendor/l5x/lib/model/index.js +19 -0
  109. package/vendor/l5x/lib/model/module.d.ts +37 -0
  110. package/vendor/l5x/lib/model/module.js +54 -0
  111. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  112. package/vendor/l5x/lib/model/modules.js +55 -0
  113. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  114. package/vendor/l5x/lib/model/parse.js +116 -0
  115. package/vendor/l5x/lib/model/project.d.ts +15 -0
  116. package/vendor/l5x/lib/model/project.js +31 -0
  117. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  118. package/vendor/l5x/lib/model/routines.js +183 -0
  119. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  120. package/vendor/l5x/lib/model/tags.js +119 -0
  121. package/vendor/l5x/lib/model/types.d.ts +304 -0
  122. package/vendor/l5x/lib/model/types.js +12 -0
  123. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  124. package/vendor/l5x/lib/model/udts.js +81 -0
  125. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  126. package/vendor/l5x/lib/model/xml.js +291 -0
  127. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  128. package/vendor/l5x/lib/model/xref.js +154 -0
  129. package/vendor/l5x/lib/session.d.ts +119 -0
  130. package/vendor/l5x/lib/session.js +236 -0
  131. package/vendor/l5x/lib/skill.d.ts +30 -0
  132. package/vendor/l5x/lib/skill.js +44 -0
  133. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  134. package/vendor/l5x/lib/studio-state.js +79 -0
  135. package/vendor/l5x/lib/studio.d.ts +29 -0
  136. package/vendor/l5x/lib/studio.js +40 -0
  137. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  138. package/vendor/l5x/lib/tools/checks.js +140 -0
  139. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  140. package/vendor/l5x/lib/tools/common.js +84 -0
  141. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  142. package/vendor/l5x/lib/tools/controller.js +170 -0
  143. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  144. package/vendor/l5x/lib/tools/drafts.js +270 -0
  145. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  146. package/vendor/l5x/lib/tools/live.js +175 -0
  147. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  148. package/vendor/l5x/lib/tools/read.js +245 -0
  149. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  150. package/vendor/l5x/lib/tools/references.js +269 -0
  151. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  152. package/vendor/l5x/lib/tools/studio.js +193 -0
  153. package/vendor/l5x/poll-worker.mjs +77 -0
  154. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  155. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  156. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  157. package/vendor/mwi/README.md +60 -0
  158. package/vendor/mwi/SKILL.md +87 -0
  159. package/vendor/mwi/bridge/README.md +8 -0
  160. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  161. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  162. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  163. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  164. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  165. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  229. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  230. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  231. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  232. package/vendor/mwi/code/mw_code.py +1258 -0
  233. package/vendor/mwi/legacy/edit-session.js +50 -0
  234. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  235. package/vendor/mwi/legacy/native-code.js +83 -0
  236. package/vendor/mwi/legacy/native-groups.js +75 -0
  237. package/vendor/mwi/legacy/native-structure.js +143 -0
  238. package/vendor/mwi/legacy/native-variables.js +77 -0
  239. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  240. package/vendor/mwi/legacy/pou-package.js +66 -0
  241. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  242. package/vendor/mwi/lib/core/bridge.js +53 -0
  243. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  244. package/vendor/mwi/lib/core/code-engine.js +109 -0
  245. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  246. package/vendor/mwi/lib/core/errors.js +47 -0
  247. package/vendor/mwi/lib/core/params.d.ts +52 -0
  248. package/vendor/mwi/lib/core/params.js +46 -0
  249. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  250. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  251. package/vendor/mwi/lib/core/result.d.ts +45 -0
  252. package/vendor/mwi/lib/core/result.js +20 -0
  253. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  254. package/vendor/mwi/lib/core/tool.js +55 -0
  255. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  256. package/vendor/mwi/lib/edit/compare.js +70 -0
  257. package/vendor/mwi/lib/edit.d.ts +20 -0
  258. package/vendor/mwi/lib/edit.js +32 -0
  259. package/vendor/mwi/lib/index.d.ts +21 -0
  260. package/vendor/mwi/lib/index.js +33 -0
  261. package/vendor/mwi/lib/read/service.d.ts +117 -0
  262. package/vendor/mwi/lib/read/service.js +229 -0
  263. package/vendor/mwi/lib/read.d.ts +21 -0
  264. package/vendor/mwi/lib/read.js +33 -0
  265. package/vendor/mwi/lib/session/project.d.ts +34 -0
  266. package/vendor/mwi/lib/session/project.js +54 -0
  267. package/vendor/mwi/lib/session/service.d.ts +167 -0
  268. package/vendor/mwi/lib/session/service.js +566 -0
  269. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  270. package/vendor/mwi/lib/session/stage.js +187 -0
  271. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  272. package/vendor/mwi/lib/session/workspace.js +90 -0
  273. package/vendor/mwi/lib/skill.d.ts +36 -0
  274. package/vendor/mwi/lib/skill.js +56 -0
  275. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  276. package/vendor/mwi/lib/tools/edit.js +286 -0
  277. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  278. package/vendor/mwi/lib/tools/read.js +161 -0
  279. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  280. package/vendor/mwi/lib/tools/session.js +166 -0
  281. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  282. package/vendor/mwi/lib/tools/verify.js +85 -0
  283. package/vendor/mwi/lib/verify.d.ts +17 -0
  284. package/vendor/mwi/lib/verify.js +29 -0
@@ -0,0 +1,98 @@
1
+ """Independent saved structural/source evidence for native IDE mutations."""
2
+ import hashlib
3
+ import re
4
+ import xml.etree.ElementTree as ET
5
+ from .project import Project
6
+ from .cfb import CompoundFile
7
+ from .tree import parse_document, task_assignments
8
+ from .tasks import read_tasks
9
+ from .program_checks import mask
10
+ from .workflow import source_manifest
11
+
12
+ # Exact native empty GB emitted by MotionWorks IEC 3 Pro / Ade 1.19.
13
+ # Independently observed in two LD and two FBD creations. Unknown versions,
14
+ # truncated bodies and populated bodies must not be inferred to be empty.
15
+ EMPTY_GRAPH_SHA256 = '09985918e42e9108dd10f5d67cbafa125bd4938821c6c6efe160e735b49492bb'
16
+
17
+ def body_is_blank(language, raw, text=None):
18
+ if language in ('ST', 'IL'):
19
+ return text is not None and not text.strip()
20
+ return language in ('LD', 'FBD') and len(raw) == 292 and hashlib.sha256(raw).hexdigest() == EMPTY_GRAPH_SHA256
21
+
22
+ def snapshot(root):
23
+ project=Project(root)
24
+ document=parse_document(CompoundFile(root/'src.st1').read_stream('PROJECT.TRE').decode('latin1'))
25
+ if document.warnings:raise ValueError('Project tree parse warnings')
26
+ registry={}
27
+ for line in project.list_pou_lines():
28
+ columns=line.split('\t')
29
+ kind=columns[0].upper().replace('FUNCTIONBLOCK','FUNCTION_BLOCK')
30
+ if kind not in ('PROGRAM','FUNCTION','FUNCTION_BLOCK') or len(columns)<3:raise ValueError('Unsupported POU registry row')
31
+ if columns[1].casefold() in registry:raise ValueError('Duplicate POU registry row')
32
+ registry[columns[1].casefold()]=(kind,columns[2])
33
+ pous=[]
34
+ for pou in project.pous():
35
+ if pou.name.casefold() not in registry:raise ValueError('Unregistered POU directory')
36
+ table=pou.declarations()
37
+ if table.warnings:raise ValueError('Declaration parse warnings')
38
+ body_stream=pou.body_stream()
39
+ if not body_stream:raise ValueError('Missing POU body')
40
+ body=(pou.st_body() if body_stream[1]=='ST' else pou.text_body()) if body_stream[1] in ('ST','IL') else None
41
+ raw=body.encode('utf-8') if body is not None else pou.source().read_stream(body_stream[0])
42
+ variables=[dict(name=v.name,type=v.type_name,section=v.section,group=v.group,
43
+ address=v.address,initial_value=v.initial_value,description=v.description) for v in table.variables]
44
+ kind,_=registry[pou.name.casefold()]
45
+ parents=[node for node,_ in document.walk_with_ancestors()
46
+ if node.name.casefold()==pou.name.casefold() and
47
+ node.path.split('\t')[0].replace('\\','/').casefold()=='poe/'+pou.name.casefold()]
48
+ if len(parents)!=1:raise ValueError('POU registry/tree identity mismatch')
49
+ return_type=document.lines[parents[0].line+3].strip() if kind=='FUNCTION' else ''
50
+ if kind=='FUNCTION' and not return_type:raise ValueError('Missing function return type in saved tree')
51
+ pous.append(dict(name=pou.name,type=kind,return_type=return_type,language=body_stream[1],
52
+ body_sha256=hashlib.sha256(raw).hexdigest(),body_blank=body_is_blank(body_stream[1],raw,body),
53
+ variables=variables,identifiers=sorted(set(re.findall(r'[A-Za-z_][A-Za-z_0-9]*',mask(body or '').upper()))),
54
+ reference_scan_complete=body is not None))
55
+ if len(pous)!=len(registry):raise ValueError('Missing registered POU source')
56
+ task_nodes=[]
57
+ for node,_ in document.walk_with_ancestors():
58
+ path=node.path.split('\t')[0].replace('\\','/').split('/')
59
+ if len(path)==5 and path[0]=='C' and path[2]=='R' and path[-1]==node.name:
60
+ fields=node.path.split('\t')
61
+ instances=[]
62
+ for child in node.children:
63
+ program_type=document.lines[child.line+2].split('\t')[0]
64
+ if not program_type:raise ValueError('Missing saved program type')
65
+ instances.append(dict(name=child.name,type=program_type))
66
+ task_nodes.append(dict(name=node.name,kind=fields[3],instances=instances))
67
+ settings={t.name.casefold():t for t in read_tasks(root)}
68
+ if len(settings)!=len(task_nodes):raise ValueError('Task settings/tree inventory mismatch')
69
+ for task in task_nodes:
70
+ setting=settings[task['name'].casefold()]
71
+ if setting.warnings or setting.type!=task['kind']:raise ValueError('Task settings/tree disagree')
72
+ task['settings']=setting.fields
73
+ globals=project.global_variables()
74
+ if globals.warnings:raise ValueError('Global declaration parse warnings')
75
+ global_rows=[dict(name=v.name,type=v.type_name,section=v.section,group=v.group,
76
+ address=v.address,initial_value=v.initial_value,description=v.description) for v in globals.variables]
77
+ manifest=source_manifest(root)
78
+ program_sources={file:{name:sha for name,sha in streams.items() if name.upper().endswith(('.VB','.VGR','.STB','.AB','.GB','.TXT'))}
79
+ for file,streams in manifest['streams'].items()}
80
+ file_hashes=dict(manifest['files'])
81
+ for setting in settings.values():
82
+ file_hashes[setting.source.relative_to(root).as_posix()]=hashlib.sha256(setting.source.read_bytes()).hexdigest()
83
+ translations={}
84
+ empty_translations=[]
85
+ for pou in project.pous():
86
+ for path in pou.directory.iterdir():
87
+ if path.name.casefold().endswith('translation.xml'):
88
+ key=path.relative_to(root).as_posix()
89
+ translations[key]=hashlib.sha256(path.read_bytes()).hexdigest()
90
+ file_hashes[key]=translations[key]
91
+ try:
92
+ translation_document=ET.parse(path)
93
+ if translation_document.getroot().tag=='TranslationDocument' and not translation_document.findall('./ItemList/item'):
94
+ empty_translations.append(key)
95
+ except ET.ParseError:
96
+ pass # Older unused translation placeholders may be all zero bytes.
97
+ return dict(pous=pous,tasks=task_nodes,globals=global_rows,program_sources=program_sources,
98
+ translation_files=translations,empty_translation_files=empty_translations,file_hashes=file_hashes)
@@ -0,0 +1,68 @@
1
+ """Exact saved PROGRAM instance ownership, separate from runtime execution."""
2
+ import hashlib
3
+ import re
4
+ from pathlib import Path
5
+ from .cfb import CompoundFile
6
+ from .tree import parse_document
7
+ from .tasks import parse_task_settings
8
+
9
+ IDENT = re.compile(r'^[A-Za-z_][A-Za-z_0-9]*$')
10
+
11
+ def resolve(document, programs):
12
+ if document.warnings:
13
+ raise ValueError('Task tree parse warnings')
14
+ known = {name.casefold(): name for name in programs}
15
+ if len(known) != len(programs): raise ValueError('Duplicate PROGRAM registry identity')
16
+ tasks, identities = [], set()
17
+ for node, _ in document.walk_with_ancestors():
18
+ fields = node.path.split('\t')
19
+ path = fields[0].replace('\\', '/').split('/')
20
+ if len(path) != 5 or path[0] != 'C' or path[2] != 'R' or path[-1] != node.name:
21
+ continue
22
+ if len(fields) < 4 or fields[3] not in ('CYCLIC','DEFAULT','SYSTEM') or not all(IDENT.fullmatch(p) for p in (path[1],path[3],node.name)):
23
+ raise ValueError('Unsupported task identity/kind')
24
+ identity = '/'.join(path)
25
+ if identity.casefold() in identities: raise ValueError('Duplicate task identity')
26
+ identities.add(identity.casefold()); instances, names = [], set()
27
+ for order, child in enumerate(node.children):
28
+ if child.children or not IDENT.fullmatch(child.name) or child.name.casefold() in names:
29
+ raise ValueError('Ambiguous PROGRAM instance identity')
30
+ names.add(child.name.casefold())
31
+ program = document.lines[child.line+2].split('\t')[0]
32
+ if program.casefold() not in known:
33
+ raise ValueError('Task instance type is absent from PROGRAM registry: '+program)
34
+ instances.append(dict(name=child.name,type=known[program.casefold()],order=order+1))
35
+ tasks.append(dict(name=node.name,configuration=path[1],resource=path[3],
36
+ path=identity,kind=fields[3],instances=instances))
37
+ return tasks
38
+
39
+ def inspect(root):
40
+ root = Path(root)
41
+ registry = root/'LIST.POU'; tree = root/'src.st1'
42
+ before = {str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in (registry,tree)}
43
+ programs, seen = [], set()
44
+ for line in registry.read_text(encoding='latin1').splitlines():
45
+ if not line.strip(): continue
46
+ columns = line.split('\t')
47
+ if len(columns)<2 or columns[0].upper().replace('FUNCTIONBLOCK','FUNCTION_BLOCK') not in ('PROGRAM','FUNCTION','FUNCTION_BLOCK') or not IDENT.fullmatch(columns[1]) or columns[1].casefold() in seen:
48
+ raise ValueError('Invalid/duplicate POU registry identity')
49
+ seen.add(columns[1].casefold())
50
+ if columns[0].upper() == 'PROGRAM': programs.append(columns[1])
51
+ document = parse_document(CompoundFile(tree).read_stream('PROJECT.TRE').decode('latin1'))
52
+ tasks = resolve(document,programs)
53
+ for task in tasks:
54
+ source = root/Path(task['path']).parent/(task['name']+'.SET')
55
+ if not source.resolve().is_relative_to(root.resolve()): raise ValueError('Task settings escape project')
56
+ raw = source.read_bytes(); before[str(source)] = hashlib.sha256(raw).hexdigest()
57
+ settings = parse_task_settings(raw.decode('latin1'),source)
58
+ if settings.warnings or settings.name.casefold()!=task['name'].casefold() or settings.type!=task['kind']:
59
+ raise ValueError('Task settings/tree identity or kind mismatch')
60
+ task['settings'] = settings.fields
61
+ task['settings_source'] = str(source)
62
+ for path, digest in before.items():
63
+ if hashlib.sha256(Path(path).read_bytes()).hexdigest()!=digest:
64
+ raise ValueError('Task sources changed during read')
65
+ assigned = {instance['type'].casefold() for task in tasks for instance in task['instances']}
66
+ return dict(bindings=tasks,program_types=programs,
67
+ unassigned=sorted(name for name in programs if name.casefold() not in assigned),
68
+ binding_resolution='exact_saved_tree',source_hashes=before)
@@ -0,0 +1,162 @@
1
+ """Read MotionWorks task configuration.
2
+
3
+ Each resource task has a plain-text ``<Task>.SET`` file beside the resource
4
+ ``src.st1``::
5
+
6
+ TASK FastTsk
7
+ (TYPE := CYCLIC,
8
+ INTERVAL := T#4ms,
9
+ PRIORITY := 0,
10
+ WATCHDOG := 4
11
+ WATCHDOG_DISPLAY := 4
12
+ WATCHDOG_ENABLED := YES
13
+ );
14
+
15
+ This is useful context for anyone editing motion code. Cycle time and priority
16
+ decide whether a block can finish in its task, and the watchdog decides what
17
+ happens when it cannot -- both matter more in motion control than in ordinary PLC
18
+ logic, and neither is visible from the POU bodies alone.
19
+
20
+ The format is close to IEC structured text but is not valid ST: fields are
21
+ newline-separated inside the parentheses and only the last one carries a
22
+ semicolon, so a real ST parser rejects it. This reader is therefore its own
23
+ small field parser rather than a reuse of the declaration parser.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from dataclasses import dataclass, field
30
+ from pathlib import Path
31
+
32
+ from .errors import NotFound
33
+
34
+ #: ``TASK <name>`` on the first meaningful line.
35
+ _TASK_RE = re.compile(r"^\s*TASK\s+(\S+)", re.IGNORECASE)
36
+ #: ``FIELD := value`` with an optional trailing comma or semicolon.
37
+ _FIELD_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)[ \t]*:=[ \t]*([^,;\r\n]*)")
38
+
39
+
40
+ @dataclass
41
+ class TaskSettings:
42
+ """One task's configuration."""
43
+
44
+ name: str
45
+ source: Path
46
+ fields: dict[str, str] = field(default_factory=dict)
47
+ warnings: list[str] = field(default_factory=list)
48
+
49
+ @property
50
+ def type(self) -> str | None:
51
+ return self.fields.get("TYPE")
52
+
53
+ @property
54
+ def interval(self) -> str | None:
55
+ """Cycle time such as ``T#4ms``, for cyclic tasks."""
56
+ return self.fields.get("INTERVAL")
57
+
58
+ @property
59
+ def priority(self) -> int | None:
60
+ raw = self.fields.get("PRIORITY")
61
+ try:
62
+ return int(raw) if raw is not None else None
63
+ except ValueError:
64
+ return None
65
+
66
+ @property
67
+ def watchdog(self) -> str | None:
68
+ return self.fields.get("WATCHDOG")
69
+
70
+ @property
71
+ def watchdog_enabled(self) -> bool | None:
72
+ raw = self.fields.get("WATCHDOG_ENABLED")
73
+ if raw is None:
74
+ return None
75
+ return raw.strip().upper() in {"YES", "TRUE", "1"}
76
+
77
+ def summary(self) -> str:
78
+ bits = [f"TYPE={self.type or '?'}"]
79
+ if self.interval:
80
+ bits.append(f"cycle={self.interval}")
81
+ if self.priority is not None:
82
+ bits.append(f"priority={self.priority}")
83
+ # Report the watchdog whenever we know anything about it. Gating this on
84
+ # a WATCHDOG value alone hid the "disabled" state for tasks that set only
85
+ # WATCHDOG_ENABLED, which is exactly the case worth surfacing.
86
+ if self.watchdog or self.watchdog_enabled is not None:
87
+ state = " (disabled)" if self.watchdog_enabled is False else ""
88
+ bits.append(f"watchdog={self.watchdog or 'n/a'}{state}")
89
+ for key in ("SPG", "INTERVAL_DISPLAY"):
90
+ if key in self.fields:
91
+ bits.append(f"{key.lower()}={self.fields[key]}")
92
+ return " ".join(bits)
93
+
94
+
95
+ def parse_task_settings(text: str, source: Path | None = None) -> TaskSettings:
96
+ """Parse a ``.SET`` payload.
97
+
98
+ Raises :class:`NotFound` when the text does not begin with a ``TASK`` line,
99
+ which is how a non-task ``.SET`` file is rejected rather than mis-parsed.
100
+ """
101
+ match = _TASK_RE.search(text)
102
+ if match is None:
103
+ raise NotFound(
104
+ f"{source.name if source else '<text>'} is not a task settings file "
105
+ f"(no TASK line)"
106
+ )
107
+
108
+ settings = TaskSettings(
109
+ name=match.group(1).strip(),
110
+ source=source or Path("<text>"),
111
+ )
112
+ for field_match in _FIELD_RE.finditer(text):
113
+ key = field_match.group(1).upper()
114
+ value = field_match.group(2).strip()
115
+ if key in settings.fields:
116
+ settings.warnings.append(f"duplicate field {key!r}; kept the first")
117
+ continue
118
+ settings.fields[key] = value
119
+ return settings
120
+
121
+
122
+ def task_files(project_root: Path) -> list[Path]:
123
+ """Every ``*.SET`` that looks like a task configuration in a project."""
124
+ root = Path(project_root)
125
+ found: list[Path] = []
126
+ for pattern in ("C/Configuration/R/Resource", "C/Resource/R/Resource"):
127
+ directory = root / pattern
128
+ if directory.is_dir():
129
+ found.extend(sorted(directory.glob("*.SET")))
130
+ return found
131
+
132
+
133
+ def read_tasks(project_root: Path) -> list[TaskSettings]:
134
+ """Read every task configuration in a project.
135
+
136
+ Task files sit beside the resource ``src.st1``. Files that are not task
137
+ settings are skipped rather than treated as errors, because the same
138
+ directory holds other ``.SET`` files.
139
+ """
140
+ settings: list[TaskSettings] = []
141
+ for path in task_files(project_root):
142
+ try:
143
+ text = path.read_bytes().decode("latin1")
144
+ except OSError:
145
+ continue
146
+ try:
147
+ settings.append(parse_task_settings(text, path))
148
+ except NotFound:
149
+ continue
150
+ # Present cyclic tasks in ascending cycle time, which is how a user thinks
151
+ # about them: the fastest task is the one with the least time to spare.
152
+ def sort_key(item: TaskSettings):
153
+ interval = item.interval or ""
154
+ match = re.search(r"T#(\d+(?:\.\d+)?)(ms|us|s|ns)?", interval, re.IGNORECASE)
155
+ if not match:
156
+ return (2, 0.0, item.name)
157
+ value = float(match.group(1))
158
+ unit = (match.group(2) or "ms").lower()
159
+ scale = {"ns": 1e-6, "us": 1e-3, "ms": 1.0, "s": 1000.0}[unit]
160
+ return (0, value * scale, item.name)
161
+
162
+ return sorted(settings, key=sort_key)
@@ -0,0 +1,157 @@
1
+ """Recoverable offline transactions. A successful file write is not a build verdict."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from uuid import uuid4
12
+
13
+ from .errors import MotionWorksError
14
+ from .ide import ensure_ide_closed
15
+ from .snapshot import sha256_file
16
+ from .staging import assert_proven, workspace_root
17
+
18
+
19
+ def hash_tree(root):
20
+ """A rollback snapshot includes generated files and hidden directories too."""
21
+ return {p.relative_to(root).as_posix(): sha256_file(p)
22
+ for p in sorted(Path(root).rglob('*')) if p.is_file()}
23
+
24
+
25
+ def _save(path: Path, payload: dict) -> None:
26
+ temp = path.with_suffix('.tmp')
27
+ temp.write_text(json.dumps(payload, indent=2), encoding='utf-8')
28
+ os.replace(temp, path)
29
+
30
+
31
+ def _restore(root: Path, backup: Path, before: dict[str, str]) -> None:
32
+ # Verify the complete rollback set before replacing even one source file.
33
+ for name, digest in before.items():
34
+ if sha256_file(backup / name) != digest:
35
+ raise MotionWorksError(f'Rollback backup failed verification: {name}')
36
+ for path in sorted(root.rglob('*'), reverse=True):
37
+ if path.is_file() and path.relative_to(root).as_posix() not in before:
38
+ path.unlink()
39
+ for name in before:
40
+ target = root / name
41
+ target.parent.mkdir(parents=True, exist_ok=True)
42
+ temp = target.with_name(target.name + '.rollback-' + uuid4().hex)
43
+ shutil.copy2(backup / name, temp)
44
+ os.replace(temp, target)
45
+ for path in sorted(root.rglob('*'), reverse=True):
46
+ if path.is_dir() and not any(path.iterdir()):
47
+ path.rmdir()
48
+ if hash_tree(root) != before:
49
+ raise MotionWorksError('Rollback read-back differs from the recorded snapshot')
50
+
51
+
52
+ def update_metadata(root: Path) -> None:
53
+ path = root / 'PROJECT.INF'
54
+ if not path.exists():
55
+ return
56
+ raw = path.read_bytes()
57
+ if raw.startswith((b'\xff\xfe', b'\xfe\xff')):
58
+ encoding = 'utf-16'
59
+ else:
60
+ encoding = 'latin1'
61
+ text = raw.decode(encoding)
62
+ updated, count = re.subn(r'(?m)^LastChange=[^\r\n]*',
63
+ 'LastChange=' + datetime.now().strftime('%m/%d/%Y %I:%M:%S %p'), text)
64
+ if count != 1:
65
+ raise MotionWorksError('PROJECT.INF must have exactly one LastChange field')
66
+ temp = path.with_name(path.name + '.transaction-' + uuid4().hex)
67
+ temp.write_bytes(updated.encode(encoding))
68
+ os.replace(temp, path)
69
+
70
+
71
+ def run(project: Path, operation, *, validate=None) -> dict:
72
+ """Commit a whole operation or restore its complete pre-operation file set.
73
+
74
+ The lock remains after a process crash or failed rollback. A later writer refuses;
75
+ it never silently treats an incomplete transaction as committed.
76
+ """
77
+ root = assert_proven(project)
78
+ if root.suffix.lower() == '.mwt':
79
+ root = root.with_suffix('')
80
+ if not root.is_dir():
81
+ raise MotionWorksError('Transaction requires an expanded project directory')
82
+ ensure_ide_closed(auto_close=False)
83
+ home = workspace_root() / '.motionworks'
84
+ key = hashlib.sha256(str(root).casefold().encode()).hexdigest()[:20]
85
+ lock = home / 'locks' / (key + '.lock')
86
+ if not lock.resolve().is_relative_to(workspace_root()):
87
+ raise MotionWorksError('Transaction lock escapes the workspace')
88
+ lock.parent.mkdir(parents=True, exist_ok=True)
89
+ transaction_id = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ-') + uuid4().hex[:8]
90
+ folder = home / 'transactions' / key / transaction_id
91
+ if not folder.resolve().is_relative_to(workspace_root()):
92
+ raise MotionWorksError('Transaction journal escapes the workspace')
93
+ try:
94
+ fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
95
+ except FileExistsError as exc:
96
+ raise MotionWorksError(f'Project transaction is locked; inspect {lock} before recovery') from exc
97
+ with os.fdopen(fd, 'w') as handle:
98
+ json.dump({'pid': os.getpid(), 'journal': str(folder / 'journal.json')}, handle)
99
+ clean = False
100
+ before = None
101
+ journal = {'id': transaction_id, 'project': str(root), 'state': 'preparing',
102
+ 'verification': 'offline_only', 'required': ['rebuild', 'make', 'save', 'close_reopen']}
103
+ try:
104
+ folder.mkdir(parents=True)
105
+ before = hash_tree(root)
106
+ shutil.copytree(root, folder / 'before')
107
+ if hash_tree(folder / 'before') != before or hash_tree(root) != before:
108
+ raise MotionWorksError('Source changed during snapshot or backup did not verify')
109
+ journal.update(state='prepared', before=before)
110
+ _save(folder / 'journal.json', journal)
111
+ # Recheck after potentially slow backup, immediately before changing files.
112
+ ensure_ide_closed(auto_close=False)
113
+ journal['state'] = 'applying'
114
+ _save(folder / 'journal.json', journal)
115
+ payload = operation()
116
+ if payload.get('ok') is False:
117
+ raise MotionWorksError('Operation reported failure: ' + json.dumps(payload))
118
+ if validate:
119
+ validation = validate(root)
120
+ else:
121
+ from .validation import validate as validate_native
122
+ current = hash_tree(root)
123
+ changed_containers = [root / name for name, digest in current.items()
124
+ if Path(name).name.lower() == 'src.st1' and before.get(name) != digest]
125
+ validation = validate_native(root, containers=changed_containers) if changed_containers else {'status': 'operation_readback_only'}
126
+ if validation.get('ok') is False:
127
+ raise MotionWorksError('Static validation failed: ' + json.dumps(validation))
128
+ after = hash_tree(root)
129
+ if after != before:
130
+ update_metadata(root)
131
+ after = hash_tree(root)
132
+ changed = sorted(name for name in set(before) | set(after) if before.get(name) != after.get(name))
133
+ journal.update(state='committed', after=after, changed_files=changed, validation=validation)
134
+ _save(folder / 'journal.json', journal)
135
+ report = {k: journal[k] for k in ('id', 'state', 'verification', 'required', 'changed_files', 'validation')}
136
+ report.update(journal=str(folder / 'journal.json'), backup=str(folder / 'before'),
137
+ hashes={name: {'before': before.get(name), 'after': after.get(name)} for name in changed})
138
+ payload.setdefault('result', {})['transaction'] = report
139
+ clean = True
140
+ return payload
141
+ except Exception as exc:
142
+ if journal['state'] in ('applying', 'committed'):
143
+ try:
144
+ ensure_ide_closed(auto_close=False)
145
+ assert_proven(root)
146
+ _restore(root, folder / 'before', before)
147
+ journal.update(state='rolled_back', error=str(exc))
148
+ _save(folder / 'journal.json', journal)
149
+ clean = True
150
+ except Exception as rollback_error:
151
+ raise MotionWorksError(f'Operation failed: {exc}; rollback needs recovery: {rollback_error}; journal: {folder}') from exc
152
+ else:
153
+ clean = True # No project mutation has begun.
154
+ raise MotionWorksError(f'Offline transaction failed: {exc}; journal: {folder}') from exc
155
+ finally:
156
+ if clean:
157
+ lock.unlink(missing_ok=True)