@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,193 @@
1
+ /**
2
+ * The online-supervised entry's offline half: SDK status, ACD export, L5X
3
+ * verification, routine build into a new ACD, and review-evidence checking.
4
+ *
5
+ * No tool here contacts a controller. Upload, mode change and download belong
6
+ * to the controller tools, which require per-call human approval and are not
7
+ * part of this entry yet.
8
+ *
9
+ * @module dsh-engineering-l5x/tools/studio
10
+ */
11
+ import { createHash, randomUUID } from 'node:crypto';
12
+ import { readFile } from 'node:fs/promises';
13
+ import { L5xError } from "../core/errors.js";
14
+ import { success } from "../core/result.js";
15
+ import { object, string } from "../core/params.js";
16
+ import { editOfflineAcd, exportAcdToL5x, studioSdkStatus, verifyOfflineL5x, } from "../engine/studio-sdk.js";
17
+ import { engineeringCapabilities } from "../engine/capabilities.js";
18
+ import { routineEditPlan } from "../engine/acd-edit.js";
19
+ import { reviewDownloadSource } from "../engine/download-review.js";
20
+ import { verifyReviewEvidence } from "../engine/review-evidence.js";
21
+ import { PROJECT_ID, optionalString, requiredString, tool } from "./common.js";
22
+ /** Build verdicts come from Studio, not from this plugin. */
23
+ const STUDIO_COVERAGE = {
24
+ complete: false,
25
+ unverified: [
26
+ 'the build verdict is Studio 5000\'s; this plugin reports it',
27
+ 'runtime behavior',
28
+ 'controller state and online logic',
29
+ ],
30
+ };
31
+ /**
32
+ * The studio tools, bound to one chat's projects and export provenance.
33
+ *
34
+ * @param session - Project handles for the calling chat.
35
+ * @param state - ACD export records for the calling chat.
36
+ * @returns Tool definitions in registration order.
37
+ */
38
+ export function studioTools(session, state) {
39
+ return [
40
+ tool({
41
+ name: 'studio_sdk_status',
42
+ description: 'Preflight the Logix Designer SDK service, FactoryTalk Linx services, SDK package, .NET and installed Studio versions. Call before any SDK tool; no project or controller operation.',
43
+ parameters: object({}),
44
+ async run(_args, _exec) {
45
+ const status = await studioSdkStatus();
46
+ return success(status.available ? 'The Studio 5000 SDK is available for offline work.' : `The Studio 5000 SDK is unavailable: ${status.reason}`, status, status.available ? [{ tool: 'studio_capabilities', why: 'See what this SDK release can do for the open project.' }] : [], STUDIO_COVERAGE);
47
+ },
48
+ }),
49
+ tool({
50
+ name: 'studio_capabilities',
51
+ description: 'Report offline, SDK, revision-specific build, reference and live-read capabilities with next steps. Optional project_id checks the selected revision; no controller contact.',
52
+ parameters: object({ project_id: PROJECT_ID }, []),
53
+ async run(args, exec) {
54
+ const status = await studioSdkStatus();
55
+ const projectArg = optionalString(args, 'project_id');
56
+ const project = projectArg === undefined ? undefined : await session.require(projectArg, exec);
57
+ return success(`Studio capabilities for ${project?.document.controller.name ?? 'this machine'}.`, engineeringCapabilities(status, project), [{ tool: 'studio_sdk_status', why: 'Inspect the SDK services in detail.' }], STUDIO_COVERAGE);
58
+ },
59
+ }),
60
+ tool({
61
+ name: 'export_acd_to_l5x',
62
+ description: 'Export one existing offline ACD to a new full L5X with the running Logix Designer SDK service. No controller connection or upload; refuses to overwrite.',
63
+ parameters: object({
64
+ acd_path: string('Existing offline ACD. Example: "C:/Projects/Line/Line.ACD".'),
65
+ output_path: string('New .L5X path to write. It must not exist.'),
66
+ }, ['acd_path', 'output_path']),
67
+ async run(args, exec) {
68
+ const acdPath = await session.resolvePath(requiredString(args, 'acd_path'), exec);
69
+ const outputPath = await session.resolvePath(requiredString(args, 'output_path'), exec);
70
+ const result = await exportAcdToL5x(acdPath, outputPath, exec.signal);
71
+ state.record(exec, {
72
+ acdPath: result.source_acd,
73
+ l5xPath: result.output_path,
74
+ acdSha256: result.source_sha256,
75
+ l5xSha256: result.output_sha256,
76
+ });
77
+ return success(`Exported ${result.source_acd} to ${result.output_path}.`, result, [{ tool: 'open_project', args: { path: result.output_path }, why: 'Open the exported L5X to read it.' }], STUDIO_COVERAGE);
78
+ },
79
+ }),
80
+ tool({
81
+ name: 'verify_offline_l5x',
82
+ description: 'Import a full L5X into a new offline ACD and build it with the Studio SDK. Build needs project revision 37+; probe=true attempts a lower revision and reports the SDK error. No controller operation.',
83
+ parameters: object({
84
+ project_id: PROJECT_ID,
85
+ baseline_id: string('Project handle of the unchanged baseline export this draft came from. Example: "3f2c...".'),
86
+ output_path: string('New .ACD path to write. It must not exist.'),
87
+ probe: { type: 'boolean', default: false, description: 'Attempt the build on a revision below 37 and report the SDK error instead of refusing early.' },
88
+ }, ['baseline_id', 'output_path']),
89
+ async run(args, exec) {
90
+ const project = await session.require(optionalString(args, 'project_id'), exec);
91
+ const baseline = await session.require(requiredString(args, 'baseline_id'), exec);
92
+ const review = reviewDownloadSource(baseline, project);
93
+ const captured = state.forL5x(exec, baseline.path);
94
+ const baselineVerified = captured?.l5xSha256 === baseline.sha256;
95
+ const outputPath = await session.resolvePath(requiredString(args, 'output_path'), exec);
96
+ const build = await verifyOfflineL5x(project, outputPath, exec.signal, { probeBuild: args.probe === true });
97
+ const blockers = [
98
+ ...review.blockers,
99
+ ...(baselineVerified ? [] : ['Baseline is not an L5X exported from an ACD in this chat']),
100
+ ];
101
+ const eligible = build.offline_build === 'passed' && blockers.length === 0 && build.output_path !== null && build.output_sha256 !== null;
102
+ const verificationId = eligible ? randomUUID() : null;
103
+ if (verificationId !== null) {
104
+ state.addVerified(exec, verificationId, {
105
+ path: build.output_path,
106
+ sha256: build.output_sha256,
107
+ baselinePath: baseline.path,
108
+ sourcePath: project.path,
109
+ review: { baseline_sha256: review.baseline_sha256, source_sha256: review.source_sha256 },
110
+ });
111
+ }
112
+ return success(`Offline build ${build.offline_build}; ${blockers.length} blocker(s).`, {
113
+ ...build,
114
+ baseline_sha256: baseline.sha256,
115
+ baseline_acd_sha256: captured?.acdSha256 ?? null,
116
+ change_count: review.change_count,
117
+ change_summary: review.changes.slice(0, 30).map(change => ({ kind: change.kind, component: change.component, key: change.key })),
118
+ safety_changed: review.safety_changed,
119
+ blockers,
120
+ download_ready: eligible,
121
+ verification_id: verificationId,
122
+ }, [], STUDIO_COVERAGE);
123
+ },
124
+ }),
125
+ tool({
126
+ name: 'build_acd_from_draft',
127
+ description: 'Import one changed existing RLL or ST routine into a NEW offline ACD copy, build it, export it, and check preservation. No controller connection; requires the original ACD and a baseline exported in this chat.',
128
+ parameters: object({
129
+ acd_path: string('Original offline ACD the baseline was exported from.'),
130
+ baseline_id: string('Project handle of the unchanged baseline export.'),
131
+ project_id: PROJECT_ID,
132
+ program: string('Program that owns the changed routine. Example: "MainProgram".'),
133
+ routine: string('Routine whose edited logic is imported. Example: "MainRoutine".'),
134
+ output_path: string('New .ACD path to write. It must not exist.'),
135
+ result_export_path: string('New .L5X path for the built ACD export. It must not exist.'),
136
+ }, ['acd_path', 'baseline_id', 'program', 'routine', 'output_path', 'result_export_path']),
137
+ async run(args, exec) {
138
+ const baseline = await session.require(requiredString(args, 'baseline_id'), exec);
139
+ const draft = await session.require(optionalString(args, 'project_id'), exec);
140
+ const program = requiredString(args, 'program');
141
+ const routine = requiredString(args, 'routine');
142
+ routineEditPlan(baseline, draft, program, routine);
143
+ const acdPath = await session.resolvePath(requiredString(args, 'acd_path'), exec);
144
+ const acdSha256 = createHash('sha256').update(await readFile(acdPath)).digest('hex');
145
+ const captured = state.forL5x(exec, baseline.path);
146
+ if (captured?.acdSha256 !== acdSha256 || captured.l5xSha256 !== baseline.sha256) {
147
+ throw new L5xError('BASELINE_MISMATCH', 'The baseline was not exported from this ACD in this chat, or either file has changed since.', 'Export the source ACD with export_acd_to_l5x, open that exact L5X, and keep both files unchanged.');
148
+ }
149
+ const outputPath = await session.resolvePath(requiredString(args, 'output_path'), exec);
150
+ const exportPath = await session.resolvePath(requiredString(args, 'result_export_path'), exec);
151
+ const build = await editOfflineAcd(acdPath, draft, program, routine, outputPath, exportPath, exec.signal);
152
+ const review = reviewDownloadSource(baseline, draft);
153
+ const passed = build.offline_build === 'passed' && build.result_export_path !== null && build.output_path !== null && build.output_sha256 !== null;
154
+ const eligible = passed && review.blockers.length === 0;
155
+ const verificationId = eligible ? randomUUID() : null;
156
+ if (verificationId !== null) {
157
+ state.addVerified(exec, verificationId, {
158
+ path: build.output_path,
159
+ sha256: build.output_sha256,
160
+ baselinePath: baseline.path,
161
+ sourcePath: draft.path,
162
+ review: { baseline_sha256: review.baseline_sha256, source_sha256: review.source_sha256 },
163
+ });
164
+ }
165
+ return success(`Offline ACD build ${build.offline_build}${build.failure_stage === null ? '' : ` at ${build.failure_stage}`}.`, {
166
+ ...build,
167
+ change_count: review.change_count,
168
+ blockers: review.blockers,
169
+ download_ready: eligible,
170
+ verification_id: verificationId,
171
+ controller_operation: 'none',
172
+ next_step: eligible
173
+ ? 'Download the built ACD with download_project and its verification_id after per-call approval.'
174
+ : passed
175
+ ? 'Read the review blockers; the build is not eligible for download.'
176
+ : 'Read the build log; the source ACD was preserved and a failed output is not verified.',
177
+ }, [], STUDIO_COVERAGE);
178
+ },
179
+ }),
180
+ tool({
181
+ name: 'studio_review_evidence',
182
+ description: 'Before reporting Studio findings, verify exact program, routine and rung or ST line names with verbatim quotes. Does not verify interpretation or runtime behavior.',
183
+ parameters: object({
184
+ project_id: PROJECT_ID,
185
+ citations: string('Citations to verify, one per line or as JSON. Example: "MainProgram/MainRoutine rung 4: XIC(Start)OTE(Run);"'),
186
+ }, ['citations']),
187
+ async run(args, exec) {
188
+ const project = await session.require(optionalString(args, 'project_id'), exec);
189
+ return success('Review evidence checked against the export.', verifyReviewEvidence(project, requiredString(args, 'citations')), [], STUDIO_COVERAGE);
190
+ },
191
+ }),
192
+ ];
193
+ }
@@ -0,0 +1,77 @@
1
+ /** Isolated, read-only EtherNet/IP sampler. Parent owns deadlines and awaited termination. */
2
+ import { isMainThread, parentPort, workerData } from 'node:worker_threads'
3
+ import { setTimeout as delay } from 'node:timers/promises'
4
+ import { performance } from 'node:perf_hooks'
5
+ import ethernetIp from 'st-ethernet-ip'
6
+ import { isIPv4 } from 'node:net'
7
+
8
+ /**
9
+ * Sample explicit scalar tags without SDK discovery, subscriptions, or writes.
10
+ * @param {object} spec Validated worker request.
11
+ * @param {(frame: object) => void} emit Frame sink.
12
+ * @returns {Promise<void>} Completion after connection cleanup.
13
+ */
14
+ export async function collect(spec, emit) {
15
+ if (!spec || typeof spec.ip !== 'string' || !isIPv4(spec.ip) || !Number.isInteger(spec.slot) || spec.slot < 0 || spec.slot > 31
16
+ || !Array.isArray(spec.tags) || spec.tags.length < 1 || spec.tags.length > 256
17
+ || !Number.isInteger(spec.samples) || spec.samples < 1 || spec.samples > 100000
18
+ || !Number.isInteger(spec.interval) || spec.interval < 1
19
+ || !Number.isInteger(spec.timeout) || spec.timeout < 1)
20
+ throw new Error('Invalid polling worker request')
21
+ const plc = new ethernetIp.Controller(false)
22
+ plc.timeout_sp = spec.timeout
23
+ // The worker lifetime bounds the driver's internal timers and sockets.
24
+ plc.on('error', error => { void error /* Driver requests report transport failures; prevent an unhandled socket event. */ })
25
+ try {
26
+ await plc.connect(spec.ip, spec.slot, false)
27
+ await plc.readControllerProps()
28
+ emit({ type: 'identity', controller: {
29
+ product_name: plc.properties.name, serial_number: plc.properties.serial_number,
30
+ revision: plc.properties.version, status: plc.properties.status,
31
+ } })
32
+ const tags = spec.tags.map(entry => {
33
+ if (!entry || typeof entry.name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(entry.name)
34
+ || !['BOOL', 'INT', 'DINT', 'REAL'].includes(entry.data_type)
35
+ || (entry.program !== undefined && (typeof entry.program !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(entry.program))))
36
+ throw new Error('Invalid polling worker tag')
37
+ return { entry, tag: new ethernetIp.Tag(entry.name, entry.program) }
38
+ })
39
+ const started = performance.now()
40
+ for (let index = 0; index < spec.samples; index++) {
41
+ const sweepStart = performance.now()
42
+ const values = []
43
+ for (const { entry, tag } of tags) {
44
+ try {
45
+ await plc.readTag(tag)
46
+ const value = tag.value
47
+ const typeMatches = tag.type === entry.data_type
48
+ const scalarMatches = entry.data_type === 'BOOL' ? typeof value === 'boolean'
49
+ : typeof value === 'number' && Number.isFinite(value)
50
+ values.push({ tag: entry.program ? `Program:${entry.program}.${entry.name}` : entry.name,
51
+ data_type: entry.data_type, actual_data_type: tag.type,
52
+ value: typeMatches && scalarMatches ? value : null,
53
+ quality: typeMatches && scalarMatches ? 'good' : 'bad', observed_at: new Date().toISOString(),
54
+ ...typeMatches && scalarMatches ? {} : { error: 'Live type or value differs from the export declaration' } })
55
+ } catch (error) {
56
+ values.push({ tag: entry.program ? `Program:${entry.program}.${entry.name}` : entry.name,
57
+ data_type: entry.data_type, value: null, quality: 'bad', observed_at: new Date().toISOString(),
58
+ error: error instanceof Error ? error.message : 'Tag read failed' })
59
+ }
60
+ }
61
+ const sweepEnd = performance.now()
62
+ emit({ type: 'sample', index, elapsed_ms: sweepStart - started, sweep_ms: sweepEnd - sweepStart,
63
+ overrun: sweepEnd - sweepStart > spec.interval, values })
64
+ if (index + 1 < spec.samples) await delay(Math.max(0, spec.interval - (performance.now() - sweepStart)))
65
+ }
66
+ } finally {
67
+ // Unconnected explicit messaging needs no Forward Close request.
68
+ const closed = new Promise(resolve => plc.once('close', resolve))
69
+ if (!plc.closed) { if (!plc.destroyed) plc.destroy(); await closed }
70
+ }
71
+ }
72
+
73
+ if (!isMainThread) {
74
+ try { await collect(workerData, frame => parentPort.postMessage(frame)) }
75
+ catch (error) { parentPort.postMessage({ type: 'failure', message: error instanceof Error ? error.message : 'Polling failed' }) }
76
+ finally { parentPort.close() }
77
+ }
@@ -0,0 +1 @@
1
+ <configuration><packageSources><clear/><add key="LocalRockwellSdk" value="C:\Users\Public\Documents\Studio 5000\Logix Designer SDK\dotnet"/><add key="nuget.org" value="https://api.nuget.org/v3/index.json"/></packageSources></configuration>
@@ -0,0 +1,304 @@
1
+ using RockwellAutomation.LogixDesigner;
2
+ using RockwellAutomation.LogixDesigner.Logging;
3
+
4
+ // Deliberately narrow: no online edits, tag setters, forces, bit toggles, or data writes.
5
+ // The plugin obtains human approval before invoking upload, mode change, or download.
6
+ if (args.Length < 3 || args.Length > 9
7
+ || !new[] { "edit-routine", "verify", "export-l5x", "read-mode", "read-dint", "read-tag", "sample-tags", "get-comm-path", "upload-new", "go-online", "change-mode", "download" }.Contains(args[0]))
8
+ {
9
+ Console.Error.WriteLine("Unsupported Studio bridge command or argument count.");
10
+ return 2;
11
+ }
12
+
13
+ // Offline-only component import. Never reuse a diagnostic/online project handle.
14
+ if (args[0] == "edit-routine")
15
+ {
16
+ if (args.Length != 9 || !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)
17
+ || !Path.GetExtension(args[2]).Equals(".l5x", StringComparison.OrdinalIgnoreCase)
18
+ || !Path.GetExtension(args[3]).Equals(".acd", StringComparison.OrdinalIgnoreCase)
19
+ || !Path.GetExtension(args[6]).Equals(".l5x", StringComparison.OrdinalIgnoreCase)
20
+ || new[] { args[4], args[5] }.Any(name => !System.Text.RegularExpressions.Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$"))) return 2;
21
+ string working = Path.GetFullPath(args[3]);
22
+ string componentFile = working + ".routine.l5x";
23
+ if (File.Exists(working) || File.Exists(args[6]) || File.Exists(componentFile)) return 2;
24
+ try
25
+ {
26
+ string Hash(string file) => Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(file)));
27
+ if (!Hash(args[1]).Equals(args[7], StringComparison.OrdinalIgnoreCase)
28
+ || !Hash(args[2]).Equals(args[8], StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Source changed before offline edit");
29
+ File.Copy(args[1], working, false);
30
+ if (!Hash(working).Equals(args[7], StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Copied ACD hash mismatch");
31
+ using LogixProject edit = await LogixProject.OpenLogixProjectAsync(working, new StdOutEventLogger());
32
+ await edit.GoOfflineAsync();
33
+ string xpath = $"Controller/Programs/Program[@Name='{args[4]}']/Routines/Routine[@Name='{args[5]}']";
34
+ await edit.PartialExportToXmlFileAsync(xpath, componentFile);
35
+ var options = new System.Xml.XmlReaderSettings { DtdProcessing = System.Xml.DtdProcessing.Prohibit, XmlResolver = null };
36
+ using var draftReader = System.Xml.XmlReader.Create(args[2], options);
37
+ var draft = System.Xml.Linq.XDocument.Load(draftReader, System.Xml.Linq.LoadOptions.PreserveWhitespace);
38
+ using var componentReader = System.Xml.XmlReader.Create(componentFile, options);
39
+ var component = System.Xml.Linq.XDocument.Load(componentReader, System.Xml.Linq.LoadOptions.PreserveWhitespace);
40
+ componentReader.Close();
41
+ draftReader.Close();
42
+ System.Xml.Linq.XElement FindRoutine(System.Xml.Linq.XDocument doc) => doc.Root!.Element("Controller")!.Element("Programs")!
43
+ .Elements("Program").Single(p => (string?)p.Attribute("Name") == args[4]).Element("Routines")!
44
+ .Elements("Routine").Single(r => (string?)r.Attribute("Name") == args[5]);
45
+ var target = FindRoutine(component);
46
+ var replacement = new System.Xml.Linq.XElement(FindRoutine(draft));
47
+ replacement.SetAttributeValue("Use", (string?)target.Attribute("Use") ?? "Target");
48
+ target.ReplaceWith(replacement);
49
+ component.Save(componentFile);
50
+ Console.WriteLine("OFFLINE_COMPONENT_IMPORT_STARTED");
51
+ await edit.PartialImportFromXmlFileAsync(xpath, componentFile, LogixProject.ImportCollisionOptions.OverwriteOnColl, false);
52
+ await edit.BuildAsync(LogixProject.RequestedBuildTarget.DefaultTarget);
53
+ await edit.SaveAsync();
54
+ await edit.SaveAsAsync(Path.GetFullPath(args[6]), true, true);
55
+ if (!Hash(args[1]).Equals(args[7], StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Original ACD changed during edit");
56
+ Console.WriteLine("OFFLINE_ACD_EDIT_SUCCEEDED");
57
+ return 0;
58
+ }
59
+ catch (Exception error)
60
+ {
61
+ Console.Error.WriteLine($"OFFLINE_ACD_EDIT_FAILED: {error.GetType().Name}: {error.Message}");
62
+ return 1;
63
+ }
64
+ }
65
+
66
+ if (args[0] == "upload-new")
67
+ {
68
+ if (args.Length != 3 || !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)
69
+ || File.Exists(args[1]) || string.IsNullOrWhiteSpace(args[2])) return 2;
70
+ try
71
+ {
72
+ using LogixProject _ = await LogixProject.UploadToNewProjectAsync(Path.GetFullPath(args[1]), args[2], new StdOutEventLogger());
73
+ Console.WriteLine("UPLOAD_TO_NEW_ACD_SUCCEEDED");
74
+ return 0;
75
+ }
76
+ catch (Exception error)
77
+ {
78
+ Console.Error.WriteLine($"UPLOAD_TO_NEW_ACD_FAILED: {error.GetType().Name}: {error.Message}");
79
+ return 1;
80
+ }
81
+ }
82
+
83
+ if (args[0] is "go-online" or "change-mode" or "download")
84
+ {
85
+ if ((args[0] == "change-mode" ? args.Length != 6 : args.Length != 4)
86
+ || !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)
87
+ || string.IsNullOrWhiteSpace(args[2])
88
+ || !System.Text.RegularExpressions.Regex.IsMatch(args[3], "^[a-fA-F0-9]{64}$")) return 2;
89
+ string actionFile = Path.GetFullPath(args[1]);
90
+ if (!File.Exists(actionFile)) return 2;
91
+ try
92
+ {
93
+ string actualHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(await File.ReadAllBytesAsync(actionFile)));
94
+ if (!actualHash.Equals(args[3], StringComparison.OrdinalIgnoreCase))
95
+ {
96
+ Console.Error.WriteLine("ACD hash changed after approval.");
97
+ return 2;
98
+ }
99
+ using LogixProject action = await LogixProject.OpenLogixProjectAsync(actionFile, new StdOutEventLogger());
100
+ await action.SetCommunicationsPathAsync(args[2]);
101
+ if (args[0] == "go-online")
102
+ {
103
+ await action.GoOnlineAsync();
104
+ Console.WriteLine("GO_ONLINE_SUCCEEDED");
105
+ return 0;
106
+ }
107
+ if (args[0] == "change-mode")
108
+ {
109
+ var observed = await action.ReadControllerModeAsync();
110
+ if (!Enum.TryParse<LogixProject.ControllerMode>(args[5], false, out var expected)
111
+ || observed != expected)
112
+ {
113
+ Console.Error.WriteLine($"Controller mode changed since approval; expected {args[5]}, observed {observed}");
114
+ return 2;
115
+ }
116
+ LogixProject.RequestedControllerMode target = args[4] switch
117
+ {
118
+ "Program" => LogixProject.RequestedControllerMode.Program,
119
+ "Run" => LogixProject.RequestedControllerMode.Run,
120
+ "Test" => LogixProject.RequestedControllerMode.Test,
121
+ _ => throw new ArgumentException("Unsupported controller mode")
122
+ };
123
+ await action.ChangeControllerModeAsync(target);
124
+ Console.WriteLine($"CONTROLLER_MODE_AFTER={await action.ReadControllerModeAsync()}");
125
+ return 0;
126
+ }
127
+ var before = await action.ReadControllerModeAsync();
128
+ if (before != LogixProject.ControllerMode.Program)
129
+ {
130
+ Console.Error.WriteLine($"Download requires Program mode; current mode: {before}");
131
+ return 2;
132
+ }
133
+ Console.WriteLine($"CONTROLLER_MODE_BEFORE={before}");
134
+ await action.DownloadAsync();
135
+ Console.WriteLine("DOWNLOAD_SUCCEEDED");
136
+ return 0;
137
+ }
138
+ catch (Exception error)
139
+ {
140
+ Console.Error.WriteLine($"CONTROLLER_ACTION_FAILED: {error.GetType().Name}: {error.Message}");
141
+ return 1;
142
+ }
143
+ }
144
+
145
+ if (args[0] == "verify" && !Path.GetExtension(args[1]).Equals(".l5x", StringComparison.OrdinalIgnoreCase)) return 2;
146
+ if (args[0] == "export-l5x" && (!Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)
147
+ || !Path.GetExtension(args[2]).Equals(".l5x", StringComparison.OrdinalIgnoreCase))) return 2;
148
+ if (args[0] == "read-dint" && !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)) return 2;
149
+ if (args[0] == "read-tag" && !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)) return 2;
150
+ if (args[0] == "sample-tags" && !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)) return 2;
151
+ if (args[0] == "read-mode" && !Path.GetExtension(args[1]).Equals(".l5x", StringComparison.OrdinalIgnoreCase)
152
+ && !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)) return 2;
153
+ if (args[0] == "get-comm-path" && !Path.GetExtension(args[1]).Equals(".l5x", StringComparison.OrdinalIgnoreCase)
154
+ && !Path.GetExtension(args[1]).Equals(".acd", StringComparison.OrdinalIgnoreCase)) return 2;
155
+ string source = Path.GetFullPath(args[1]);
156
+ if (!File.Exists(source))
157
+ {
158
+ Console.Error.WriteLine("Input L5X must exist.");
159
+ return 2;
160
+ }
161
+
162
+ try
163
+ {
164
+ if (args[0] == "sample-tags")
165
+ {
166
+ if (args.Length != 6 || string.IsNullOrWhiteSpace(args[2])
167
+ || !int.TryParse(args[4], out int count) || count is < 1 or > 20
168
+ || !int.TryParse(args[5], out int intervalMs) || intervalMs is < 250 or > 5000
169
+ || (long)count * intervalMs > 30000) return 2;
170
+ var tags = System.Text.Json.JsonSerializer.Deserialize<List<WatchTag>>(args[3]);
171
+ if (tags is null || tags.Count is < 1 or > 16 || tags.Any(tag => tag.Name is null || tag.DataType is null
172
+ || !System.Text.RegularExpressions.Regex.IsMatch(tag.Name, "^[A-Za-z_][A-Za-z0-9_]*$")
173
+ || !new[] { "BOOL", "INT", "DINT", "REAL" }.Contains(tag.DataType))) return 2;
174
+ using LogixProject monitor = await LogixProject.OpenLogixProjectAsync(source, new StdOutEventLogger());
175
+ await monitor.SetCommunicationsPathAsync(args[2]);
176
+ await monitor.GoOnlineAsync();
177
+ var clock = System.Diagnostics.Stopwatch.StartNew();
178
+ for (int sample = 0; sample < count; sample++)
179
+ {
180
+ var started = clock.ElapsedMilliseconds;
181
+ var values = new List<object>();
182
+ int successfulReads = 0;
183
+ foreach (var tag in tags)
184
+ {
185
+ string path = $"Controller/Tags/Tag[@Name='{tag.Name}']";
186
+ var readAt = DateTimeOffset.UtcNow;
187
+ try
188
+ {
189
+ object value = tag.DataType switch
190
+ {
191
+ "BOOL" => await monitor.GetTagValueBOOLAsync(path, LogixProject.OperationMode.Online),
192
+ "INT" => await monitor.GetTagValueINTAsync(path, LogixProject.OperationMode.Online),
193
+ "DINT" => await monitor.GetTagValueDINTAsync(path, LogixProject.OperationMode.Online),
194
+ "REAL" => await monitor.GetTagValueREALAsync(path, LogixProject.OperationMode.Online),
195
+ _ => throw new ArgumentException("Unsupported tag type")
196
+ };
197
+ values.Add(new { tag = tag.Name, data_type = tag.DataType, value, observed_at = readAt, quality = "good" });
198
+ successfulReads++;
199
+ }
200
+ catch (Exception error)
201
+ {
202
+ values.Add(new { tag = tag.Name, data_type = tag.DataType, value = (object?)null, observed_at = readAt,
203
+ quality = "error", error = $"{error.GetType().Name}: {error.Message}" });
204
+ if (error.Message.Contains("RxE_COMM_", StringComparison.OrdinalIgnoreCase))
205
+ {
206
+ Console.Error.WriteLine($"SAMPLING_FAILED: FactoryTalk Linx communication failed while reading {tag.Name}: {error.Message}");
207
+ return 1;
208
+ }
209
+ }
210
+ }
211
+ Console.WriteLine("SAMPLE_JSON=" + System.Text.Json.JsonSerializer.Serialize(new
212
+ {
213
+ sample, elapsed_ms = clock.ElapsedMilliseconds, sweep_ms = clock.ElapsedMilliseconds - started, values
214
+ }));
215
+ if (successfulReads == 0)
216
+ {
217
+ Console.Error.WriteLine("SAMPLING_FAILED: Every tag read failed; stopping instead of repeating the same communication error.");
218
+ return 1;
219
+ }
220
+ if (clock.ElapsedMilliseconds >= 30000 && sample + 1 < count)
221
+ {
222
+ Console.Error.WriteLine("SAMPLING_FAILED: Reads exceeded the 30 second monitoring window; reduce tags or requested rate.");
223
+ return 1;
224
+ }
225
+ if (sample + 1 < count)
226
+ {
227
+ long wait = (sample + 1L) * intervalMs - clock.ElapsedMilliseconds;
228
+ if (wait > 0) await Task.Delay((int)wait);
229
+ }
230
+ }
231
+ Console.WriteLine("SAMPLING_SUCCEEDED");
232
+ return 0;
233
+ }
234
+ if (args[0] == "export-l5x")
235
+ {
236
+ if (args.Length != 3 || File.Exists(args[2])) return 2;
237
+ using LogixProject project = await LogixProject.OpenLogixProjectAsync(source, new StdOutEventLogger());
238
+ await project.SaveAsAsync(Path.GetFullPath(args[2]), true, true);
239
+ if (new FileInfo(args[2]).Length == 0) return 1;
240
+ Console.WriteLine("EXPORTED_L5X_SUCCEEDED");
241
+ return 0;
242
+ }
243
+ if (args[0] == "get-comm-path")
244
+ {
245
+ if (args.Length != 3 || args[2] != "--read-only") return 2;
246
+ using LogixProject project = await LogixProject.OpenLogixProjectAsync(source, new StdOutEventLogger());
247
+ Console.WriteLine($"PROJECT_COMM_PATH={await project.GetCommunicationsPathAsync()}");
248
+ return 0;
249
+ }
250
+ if (args[0] == "read-mode")
251
+ {
252
+ if (args.Length != 3) return 2;
253
+ if (string.IsNullOrWhiteSpace(args[2])) return 2;
254
+ using LogixProject monitor = await LogixProject.OpenLogixProjectAsync(source, new StdOutEventLogger());
255
+ await monitor.SetCommunicationsPathAsync(args[2]);
256
+ var mode = await monitor.ReadControllerModeAsync();
257
+ Console.WriteLine($"READ_CONTROLLER_MODE={mode}");
258
+ return 0;
259
+ }
260
+
261
+ if (args[0] is "read-dint" or "read-tag")
262
+ {
263
+ if (args.Length != (args[0] == "read-tag" ? 5 : 4)
264
+ || !System.Text.RegularExpressions.Regex.IsMatch(args[3], "^[A-Za-z_][A-Za-z0-9_]*$")) return 2;
265
+ string dataType = args[0] == "read-dint" ? "DINT" : args[4];
266
+ if (!new[] { "BOOL", "INT", "DINT", "REAL" }.Contains(dataType)) return 2;
267
+ using LogixProject monitor = await LogixProject.OpenLogixProjectAsync(source, new StdOutEventLogger());
268
+ await monitor.SetCommunicationsPathAsync(args[2]);
269
+ string tagPath = $"Controller/Tags/Tag[@Name='{args[3]}']";
270
+ string value = dataType switch
271
+ {
272
+ "BOOL" => (await monitor.GetTagValueBOOLAsync(tagPath, LogixProject.OperationMode.Online)).ToString().ToLowerInvariant(),
273
+ "INT" => (await monitor.GetTagValueINTAsync(tagPath, LogixProject.OperationMode.Online)).ToString(System.Globalization.CultureInfo.InvariantCulture),
274
+ "DINT" => (await monitor.GetTagValueDINTAsync(tagPath, LogixProject.OperationMode.Online)).ToString(System.Globalization.CultureInfo.InvariantCulture),
275
+ "REAL" => (await monitor.GetTagValueREALAsync(tagPath, LogixProject.OperationMode.Online)).ToString("R", System.Globalization.CultureInfo.InvariantCulture),
276
+ _ => throw new ArgumentException("Unsupported tag type")
277
+ };
278
+ Console.WriteLine(args[0] == "read-dint" ? $"READ_DINT={value}" : $"READ_TAG={value}");
279
+ return 0;
280
+ }
281
+
282
+ if (args.Length != 3) return 2;
283
+ if (!Path.GetExtension(args[2]).Equals(".acd", StringComparison.OrdinalIgnoreCase)) return 2;
284
+ string output = Path.GetFullPath(args[2]);
285
+ if (File.Exists(output) || source.Equals(output, StringComparison.OrdinalIgnoreCase))
286
+ {
287
+ Console.Error.WriteLine("Output must be a new, distinct ACD file.");
288
+ return 2;
289
+ }
290
+ using LogixProject imported = await LogixProject.OpenLogixProjectAsync(source, new StdOutEventLogger());
291
+ await imported.SaveAsAsync(output, false, false);
292
+ using LogixProject offline = await LogixProject.OpenLogixProjectAsync(output, new StdOutEventLogger());
293
+ await offline.BuildAsync(LogixProject.RequestedBuildTarget.DefaultTarget);
294
+ await offline.SaveAsync();
295
+ Console.WriteLine("OFFLINE_BUILD_SUCCEEDED");
296
+ return 0;
297
+ }
298
+ catch (Exception error)
299
+ {
300
+ Console.Error.WriteLine($"SDK_OPERATION_FAILED: {error.GetType().Name}: {error.Message}");
301
+ return 1;
302
+ }
303
+
304
+ internal sealed record WatchTag(string Name, string DataType);
@@ -0,0 +1,11 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <OutputType>Exe</OutputType>
4
+ <TargetFramework>net8.0</TargetFramework>
5
+ <ImplicitUsings>enable</ImplicitUsings>
6
+ <Nullable>enable</Nullable>
7
+ </PropertyGroup>
8
+ <ItemGroup>
9
+ <PackageReference Include="RockwellAutomation.LogixDesigner.CSClient" Version="2.1.974" />
10
+ </ItemGroup>
11
+ </Project>
@@ -0,0 +1,60 @@
1
+ # dsh-engineering-mwi
2
+
3
+ MotionWorks IEC 3 Pro tools, redesigned for the `dsh-engineering` bundle. The product works on a
4
+ **staged copy** of a workspace project and drives the running IDE through the existing 32-bit COM
5
+ bridge; the tool surface is new.
6
+
7
+ This package is **not mounted yet**. The legacy `dsh-tool-motionworks-iec` remains the shipped
8
+ surface until the redesign flips in Phase 5. The migration design and phase order are in
9
+ [docs/port/mwi.md](../../docs/port/mwi.md).
10
+
11
+ ## Rows and progress
12
+
13
+ | Row | Default | Tools | Status |
14
+ |---|---|---|---|
15
+ | `mwi-session` | on | 7 | implemented and live-verified |
16
+ | `mwi-read` | on | 7 | implemented and live-verified |
17
+ | `mwi-verify` | on | 3 | next |
18
+ | `mwi-edit` | off | 7 | planned |
19
+ | `mwi-skill` | on | the `motionworks-iec` guidance | implemented |
20
+
21
+ Read tools: `mw_project_overview`, `mw_list`, `mw_get`, `mw_search`, `mw_references`, `mw_review`, `mw_help`.
22
+
23
+ Session tools: `mw_project_find`, `mw_ide_status`, `mw_project_stage`, `mw_project_open`,
24
+ `mw_ide_start`, `mw_worksheet_open`, `mw_project_close`.
25
+
26
+ ## Live evidence
27
+
28
+ Verified against MotionWorks IEC 3 Pro 1.19 on 2026-10-23 with a copy of a real project in a temporary
29
+ workspace:
30
+
31
+ 1. `stage` copied the project, bound the wrapper to the staged directory and wrote its identity.
32
+ 2. `start` launched the IDE and answered the trial prompt (`posted_bm_click`).
33
+ 3. `status` reported the staged project open, `in_stage: true`.
34
+ 4. `worksheet` opened `Global_Variables` and verified the active editor and unchanged modified state.
35
+ 5. `close` reported `closed: true`, and the next status reported the IDE not running.
36
+
37
+ ## Structure
38
+
39
+ ```
40
+ SKILL.md the redesigned guidance
41
+ bridge/ the 32-bit PowerShell COM bridge
42
+ code/ the Python engine (wrapper binding, saved-model reads, validation)
43
+ src/core/ params, result, errors, tool, bridge, PowerShell transport, code engine
44
+ src/session/ workspace, stage, runtime
45
+ src/tools/ session row (read/verify/edit rows next)
46
+ test/ contract tests and the live fixtures
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ Windows, MotionWorks IEC 3 Pro (licensed or trial), Python 3 for the code engine, Windows 32-bit
52
+ PowerShell for the COM bridge, and a computer-use companion for desktop-only operations.
53
+
54
+ ## Develop
55
+
56
+ ```sh
57
+ pnpm install
58
+ pnpm run build
59
+ pnpm test
60
+ ```