@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,704 @@
1
+ """Resolve, inventory, and read MotionWorks IEC projects on disk.
2
+
3
+ A MotionWorks project on disk is an *expanded directory* plus a small sibling
4
+ ``.mwt`` file. In the sample projects the ``.mwt`` is only 4096 bytes: it is a
5
+ wrapper, not the project. Everything editable lives in the expanded directory::
6
+
7
+ <Project>/
8
+ src.st1 project tree + view state (CFB)
9
+ LIST.POU textual POU list
10
+ NODES.LST node/task assignment list
11
+ @LIBRARY.LST library references
12
+ PROJECT.INF last-change stamp
13
+ NODES.LST ...
14
+ C/ configuration (holds VAR_GLOBAL + IO config)
15
+ DT/ HW/ LIB/ data types, hardware, library nodes
16
+ POE/<POU>/ one directory per POU
17
+ src.st1 <POU>T.TXT, <POU>V.VGR, <POU>V.VB, <POU>.STB|.GB
18
+ NodeProperties.xml worksheet GUIDs
19
+ <POU>.CCI POU descriptor
20
+ *.xml translation documents
21
+
22
+ Two layout generations appear in the sample data: ``C/Configuration/R/Resource``
23
+ and the older ``C/Resource/R/Resource``. Both are handled by searching for the
24
+ resource directory rather than assuming a fixed path.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+
33
+ from .cfb import CfbError, CompoundFile
34
+ from .errors import (
35
+ AmbiguousProject,
36
+ NotFound,
37
+ ProjectNotFound,
38
+ UnsupportedFormat,
39
+ )
40
+ from .variables import (
41
+ DeclarationTable,
42
+ Variable,
43
+ cross_check,
44
+ decode_declarations,
45
+ read_grid_variable_count,
46
+ )
47
+
48
+ #: Extensions that mark a directory as an expanded MotionWorks project.
49
+ _ROOT_MARKERS = ("src.st1", "LIST.POU", "PROJECT.INF")
50
+
51
+ #: Default directory searched when a caller passes only a project name.
52
+ DEFAULT_SEARCH_ROOT = "Yaskawa"
53
+
54
+ _GLOBAL_STREAM_RE = re.compile(r"^GLOBAL_VARIABLES", re.IGNORECASE)
55
+
56
+
57
+ @dataclass
58
+ class PouInfo:
59
+ """One POU directory."""
60
+
61
+ name: str
62
+ directory: Path
63
+
64
+ @property
65
+ def source_path(self) -> Path:
66
+ return self.directory / "src.st1"
67
+
68
+ @property
69
+ def node_properties_path(self) -> Path:
70
+ return self.directory / "NodeProperties.xml"
71
+
72
+ def source(self) -> CompoundFile:
73
+ return CompoundFile(self.source_path)
74
+
75
+ # -- stream discovery -------------------------------------------------
76
+
77
+ def stream_names(self) -> list[str]:
78
+ try:
79
+ return self.source().stream_names()
80
+ except CfbError:
81
+ return []
82
+
83
+ def body_stream(self) -> tuple[str, str] | None:
84
+ """Return ``(stream_name, language)`` for the POU body.
85
+
86
+ Graphical streams share the .GB extension. Resolve LD/FBD from the
87
+ saved worksheet node; the extension alone cannot distinguish them.
88
+ """
89
+ streams = self.stream_names()
90
+ il = [name for name in streams if name.upper().endswith('.AB')]
91
+ if il:
92
+ if len(il) != 1 or any(name.upper().endswith(('.STB', '.GB')) for name in streams):
93
+ raise ValueError('IL body stream is ambiguous')
94
+ from .tree import parse_document
95
+ root = self.directory.parent.parent
96
+ document = parse_document(CompoundFile(root/'src.st1').read_stream('PROJECT.TRE').decode('latin1'))
97
+ expected = ('POE/'+self.name+'/'+il[0]).casefold()
98
+ matches = [node for node,_ in document.walk_with_ancestors()
99
+ if node.path.split('\t')[0].replace('\\','/').casefold() == expected]
100
+ if document.warnings or len(matches) != 1 or document.lines[matches[0].start_line].strip() != '9':
101
+ raise ValueError('IL worksheet tree identity absent or ambiguous')
102
+ return il[0], 'IL'
103
+ for name in streams:
104
+ if name.upper().endswith(".STB"):
105
+ return name, "ST"
106
+ for name in streams:
107
+ if name.upper().endswith(".GB"):
108
+ from .tree import parse_document
109
+ root = self.directory.parent.parent
110
+ document = parse_document(CompoundFile(root / 'src.st1').read_stream('PROJECT.TRE').decode('latin1'))
111
+ expected = ('POE/' + self.name + '/' + name).casefold()
112
+ matches = [node for node, _ in document.walk_with_ancestors()
113
+ if node.path.split('\t')[0].replace('\\', '/').casefold() == expected]
114
+ if document.warnings or len(matches) != 1:
115
+ raise ValueError('Graphical worksheet tree identity absent or ambiguous')
116
+ # The leading record kind is 11=LD / 12=FBD. node_id is
117
+ # the separate, project-specific handle in the params line.
118
+ record_kind = document.lines[matches[0].start_line].strip()
119
+ return name, {'11': 'LD', '12': 'FBD'}.get(record_kind, 'GRAPHICAL')
120
+ return None
121
+
122
+ def language(self) -> str | None:
123
+ found = self.body_stream()
124
+ return found[1] if found else None
125
+
126
+ def declaration_stream_name(self) -> str | None:
127
+ for name in self.stream_names():
128
+ if name.upper().endswith("V.VB"):
129
+ return name
130
+ return None
131
+
132
+ def grid_stream_name(self) -> str | None:
133
+ for name in self.stream_names():
134
+ if name.upper().endswith("V.VGR"):
135
+ return name
136
+ return None
137
+
138
+ def comment_stream_name(self) -> str | None:
139
+ for name in self.stream_names():
140
+ if name.upper().endswith("T.TXT"):
141
+ return name
142
+ return None
143
+
144
+ # -- content ----------------------------------------------------------
145
+
146
+ def declarations(self) -> DeclarationTable:
147
+ cfb = self.source()
148
+ stream_name = self.declaration_stream_name()
149
+ if stream_name is None:
150
+ return DeclarationTable(
151
+ warnings=["no <POU>V.VB declaration stream present"],
152
+ source_stream=None,
153
+ )
154
+ table = decode_declarations(cfb.read_stream(stream_name), stream_name)
155
+ grid_name = self.grid_stream_name()
156
+ if grid_name:
157
+ table.warnings.extend(
158
+ cross_check(table, read_grid_variable_count(cfb.read_stream(grid_name)))
159
+ )
160
+ return table
161
+
162
+ def st_body(self) -> str | None:
163
+ """Return the Structured Text body, or None for a graphical POU."""
164
+ found = self.body_stream()
165
+ if not found or found[1] != "ST":
166
+ return None
167
+ return self.source().read_stream(found[0]).decode("latin1")
168
+
169
+ def st_body_text(self) -> str | None:
170
+ """Return readable ST, restoring comments from native translations."""
171
+ return self.text_body_text() if self.language() == 'ST' else None
172
+
173
+ def text_body(self) -> str | None:
174
+ """Read supported ST/IL source without interpreting graphical bytes."""
175
+ found = self.body_stream()
176
+ if not found or found[1] not in ('ST', 'IL'):
177
+ return None
178
+ body = self.source().read_stream(found[0]).decode('latin1')
179
+ if '\0' in body:
180
+ raise ValueError('Text worksheet contains unsupported binary data')
181
+ return body
182
+
183
+ def text_body_text(self) -> str | None:
184
+ """Return readable ST/IL with exact native comment translations."""
185
+ body = self.text_body()
186
+ if body is None or '\x07' not in body:
187
+ return body
188
+ from .st_comments import resolve_comments
189
+ worksheet = self.body_stream()[0].rsplit('.', 1)[0]
190
+ matches = [p for p in self.directory.iterdir()
191
+ if p.name.casefold() == (worksheet + 'Translation.xml').casefold()]
192
+ if len(matches) != 1:
193
+ raise ValueError('Native ST comment translation is absent or ambiguous')
194
+ return resolve_comments(body, matches[0])
195
+
196
+ def summary(self) -> dict[str, object]:
197
+ try:
198
+ stream_names = self.stream_names()
199
+ except CfbError as exc:
200
+ return {"name": self.name, "error": str(exc)}
201
+ body = self.body_stream()
202
+ return {
203
+ "name": self.name,
204
+ "language": body[1] if body else None,
205
+ "body_stream": body[0] if body else None,
206
+ "streams": len(stream_names),
207
+ "directory": str(self.directory),
208
+ }
209
+
210
+
211
+ @dataclass
212
+ class Project:
213
+ """An expanded MotionWorks project directory."""
214
+
215
+ root: Path
216
+ _pous: list[PouInfo] | None = field(default=None, repr=False)
217
+
218
+ def __post_init__(self) -> None:
219
+ # Accept str for convenience: every path operation below assumes a Path,
220
+ # and a raw string would fail late and confusingly.
221
+ if not isinstance(self.root, Path):
222
+ self.root = Path(self.root)
223
+
224
+ # -- identity ---------------------------------------------------------
225
+
226
+ @property
227
+ def name(self) -> str:
228
+ return self.root.name
229
+
230
+ @property
231
+ def mwt_path(self) -> Path | None:
232
+ candidate = self.root.with_suffix(".mwt")
233
+ return candidate if candidate.is_file() else None
234
+
235
+ @property
236
+ def node_list_path(self) -> Path | None:
237
+ for candidate in (self.root / "NODES.LST",):
238
+ if candidate.is_file():
239
+ return candidate
240
+ return None
241
+
242
+ # -- raw file reads (ANSI/latin1, as MotionWorks writes them) ---------
243
+
244
+ def _read_text(self, name: str) -> str | None:
245
+ path = self.root / name
246
+ if not path.is_file():
247
+ return None
248
+ return path.read_bytes().decode("latin1")
249
+
250
+ def list_pou_lines(self) -> list[str]:
251
+ text = self._read_text("LIST.POU")
252
+ return [line for line in (text or "").splitlines() if line.strip()]
253
+
254
+ def library_entries(self) -> list[str]:
255
+ text = self._read_text("@LIBRARY.LST")
256
+ if not text:
257
+ return []
258
+ # First line is a version banner ("Library List, V40").
259
+ return [line for line in text.splitlines()[1:] if line.strip()]
260
+
261
+ def node_lines(self) -> list[str]:
262
+ text = self._read_text("NODES.LST")
263
+ return [line for line in (text or "").splitlines() if line.strip()]
264
+
265
+ def last_change(self) -> str | None:
266
+ text = self._read_text("PROJECT.INF")
267
+ if not text:
268
+ return None
269
+ for line in text.splitlines():
270
+ if line.lower().startswith("lastchange"):
271
+ return line.split("=", 1)[-1].strip()
272
+ return None
273
+
274
+ def project_tree_streams(self) -> dict[str, int]:
275
+ path = self.root / "src.st1"
276
+ if not path.is_file():
277
+ return {}
278
+ return CompoundFile(path).summary()
279
+
280
+ # -- POUs -------------------------------------------------------------
281
+
282
+ @property
283
+ def pou_directory(self) -> Path:
284
+ return self.root / "POE"
285
+
286
+ def pous(self) -> list[PouInfo]:
287
+ if self._pous is None:
288
+ base = self.pou_directory
289
+ if not base.is_dir():
290
+ self._pous = []
291
+ else:
292
+ self._pous = [
293
+ PouInfo(name=child.name, directory=child)
294
+ for child in sorted(base.iterdir())
295
+ if child.is_dir() and (child / "src.st1").is_file()
296
+ ]
297
+ return self._pous
298
+
299
+ def pou(self, name: str) -> PouInfo:
300
+ target = name.casefold()
301
+ for pou in self.pous():
302
+ if pou.name.casefold() == target:
303
+ return pou
304
+ available = ", ".join(p.name for p in self.pous()) or "<none>"
305
+ raise NotFound(f"No POU named {name!r} in {self.name}. Available: {available}")
306
+
307
+ # -- tasks ------------------------------------------------------------
308
+
309
+ def task_assignments(self) -> dict[str, list[str]]:
310
+ """Map task name -> assigned program names.
311
+
312
+ Read from the project tree, which is authoritative. ``NODES.LST`` is the
313
+ fallback, and is genuinely worse: ``RotaryKnife_ASP_v350`` has no
314
+ ``NODES.LST`` at all, so a list-only reader reports no tasks for a project
315
+ that has four.
316
+
317
+ In the tree a task node's path points at the resource task directory and
318
+ its final component equals its name (``...\\Resource\\FastTsk`` for the
319
+ node ``FastTsk``); matching the directory alone also catches
320
+ ``Global_Variables`` and ``IO_Configuration``, whose paths live there but
321
+ carry a file extension.
322
+ """
323
+ from_tree = self._task_assignments_from_tree()
324
+ if from_tree:
325
+ return from_tree
326
+ return self._task_assignments_from_node_list()
327
+
328
+ def _task_assignments_from_tree(self) -> dict[str, list[str]]:
329
+ from .tree import TreeParseError, parse_tree, task_assignments
330
+
331
+ path = self.root / "src.st1"
332
+ if not path.is_file():
333
+ return {}
334
+ try:
335
+ text = CompoundFile(path).read_stream("PROJECT.TRE").decode("latin1")
336
+ roots, _ = parse_tree(text)
337
+ except (OSError, ValueError, TreeParseError):
338
+ return {}
339
+ return task_assignments(roots)
340
+
341
+ def _task_assignments_from_node_list(self) -> dict[str, list[str]]:
342
+ """Read assignments from ``NODES.LST`` files.
343
+
344
+ Loose lines look like::
345
+
346
+ TASK 5 MedTsk eCLR MP2600iec
347
+ PROGRAM 6 ServoHoming ServoHoming eCLR MP2600iec
348
+
349
+ Both the project root and the resource directory are checked, because
350
+ projects differ in where they keep the file. Where both exist they
351
+ describe the same instances, so programs are de-duplicated in order --
352
+ without that, every program is reported twice.
353
+ """
354
+ assignments: dict[str, list[str]] = {}
355
+ sources = [self.node_list_path]
356
+ for extra in (
357
+ self.root / "C/Configuration/R/Resource/NODES.LST",
358
+ self.root / "C/Resource/R/Resource/NODES.LST",
359
+ ):
360
+ sources.append(extra)
361
+
362
+ for source in sources:
363
+ if source is None or not source.is_file():
364
+ continue
365
+ try:
366
+ lines = source.read_bytes().decode("latin1").splitlines()
367
+ except OSError:
368
+ continue
369
+ current_task: str | None = None
370
+ for line in lines:
371
+ if not line.strip():
372
+ continue
373
+ parts = line.split("\t")
374
+ kind = parts[0].strip().upper()
375
+ if kind == "TASK" and len(parts) >= 3:
376
+ current_task = parts[2].strip()
377
+ assignments.setdefault(current_task, [])
378
+ elif kind == "PROGRAM" and len(parts) >= 3 and current_task:
379
+ program = parts[2].strip()
380
+ seen = assignments[current_task]
381
+ if program not in seen:
382
+ seen.append(program)
383
+ return assignments
384
+
385
+ # -- variables --------------------------------------------------------
386
+
387
+ def global_variables(self) -> DeclarationTable:
388
+ """Read ``VAR_GLOBAL`` declarations from the resource source.
389
+
390
+ If the resource stores only the binary ``.VGR`` grid (as the newer
391
+ TopCutter and RotaryKnife projects do), the textual declarations are
392
+ absent and that is reported rather than invented.
393
+ """
394
+ resource = self.resource_source()
395
+ if resource is None:
396
+ return DeclarationTable(warnings=["no resource src.st1 found"])
397
+
398
+ cfb = CompoundFile(resource)
399
+ names = cfb.stream_names()
400
+ # The resource-level stream is named "Global_Variables.VB", which does
401
+ # not end in "V.VB" (that pattern is for "<POU>V.VB"), so match on the
402
+ # plain extension here.
403
+ decl_names = [n for n in names if n.upper().endswith(".VB")]
404
+ grid_names = [n for n in names if n.upper().endswith(".VGR")]
405
+
406
+ if not decl_names:
407
+ note = "resource src.st1 has no textual declaration stream"
408
+ if grid_names:
409
+ count = read_grid_variable_count(cfb.read_stream(grid_names[0]))
410
+ note += (
411
+ f"; the binary grid {grid_names[0]!r} records {count} variables, "
412
+ f"but their names and types are not stored as text"
413
+ )
414
+ return DeclarationTable(warnings=[note], source_stream=None)
415
+
416
+ table = decode_declarations(cfb.read_stream(decl_names[0]), decl_names[0])
417
+ if grid_names:
418
+ table.warnings.extend(
419
+ cross_check(table, read_grid_variable_count(cfb.read_stream(grid_names[0])))
420
+ )
421
+ return table
422
+
423
+ def resource_source(self) -> Path | None:
424
+ """Locate the resource ``src.st1`` that holds ``VAR_GLOBAL`` data.
425
+
426
+ Handles both observed layouts without hardcoding either one.
427
+ """
428
+ for pattern in (
429
+ "C/Configuration/R/Resource/src.st1",
430
+ "C/Resource/R/Resource/src.st1",
431
+ ):
432
+ candidate = self.root / pattern
433
+ if candidate.is_file():
434
+ return candidate
435
+ # Fall back to a search, preferring a directory named Resource.
436
+ matches = [
437
+ p for p in self.root.glob("C/**/src.st1")
438
+ if p.parent.name.lower() == "resource"
439
+ ]
440
+ return matches[0] if matches else None
441
+
442
+ # -- reporting --------------------------------------------------------
443
+
444
+ def inventory(self) -> dict[str, object]:
445
+ pous = self.pous()
446
+ languages: dict[str, int] = {}
447
+ for pou in pous:
448
+ languages[pou.language() or "?"] = languages.get(pou.language() or "?", 0) + 1
449
+ return {
450
+ "name": self.name,
451
+ "root": str(self.root),
452
+ "mwt": str(self.mwt_path) if self.mwt_path else None,
453
+ "last_change": self.last_change(),
454
+ "pou_count": len(pous),
455
+ "languages": languages,
456
+ "tasks": self.task_assignments(),
457
+ "libraries": self.library_entries(),
458
+ "tree_streams": self.project_tree_streams(),
459
+ "resource_source": str(self.resource_source()) if self.resource_source() else None,
460
+ }
461
+
462
+
463
+ # ---------------------------------------------------------------------------
464
+ # Discovery
465
+ # ---------------------------------------------------------------------------
466
+
467
+
468
+ def looks_like_project(directory: Path) -> bool:
469
+ """True when ``directory`` holds an expanded MotionWorks project."""
470
+ if not directory.is_dir():
471
+ return False
472
+ if (directory / "POE").is_dir():
473
+ return True
474
+ return any((directory / marker).is_file() for marker in _ROOT_MARKERS)
475
+
476
+
477
+ def discover(search_root: str | Path) -> list[Path]:
478
+ """Return every expanded project directory under ``search_root``.
479
+
480
+ A directory qualifies when it contains a ``POE`` subdirectory (this is the
481
+ reliable marker: every sample project has one, including ones missing some
482
+ root metadata files).
483
+ """
484
+ base = Path(search_root)
485
+ if not base.is_absolute():
486
+ base = Path.cwd() / base
487
+ if not base.is_dir():
488
+ return []
489
+ found = {p.parent for p in base.rglob("POE") if p.is_dir()}
490
+ return sorted(found)
491
+
492
+
493
+ def _match_by_name(base: Path, wanted: str) -> list[Path]:
494
+ """Match projects by name, supporting a disambiguating path suffix.
495
+
496
+ Two of the sample projects are both called ``TopCutter`` (one at the top
497
+ level, one under ``MP2600iec Program``), so a bare name is genuinely
498
+ ambiguous. Callers can qualify it::
499
+
500
+ TopCutter -> matches both
501
+ MP2600iec Program/TopCutter -> matches one
502
+ Yaskawa/TopCutter -> matches one
503
+
504
+ A supplied multi-segment query must match the project's trailing path
505
+ components, which is unambiguous without needing an absolute path.
506
+ """
507
+ parts = [
508
+ p.casefold()
509
+ for p in re.split(r"[\\/]+", wanted.strip())
510
+ if p and p not in (".", "..")
511
+ ]
512
+ if not parts:
513
+ return []
514
+ matches: list[Path] = []
515
+ for candidate in discover(base):
516
+ tail = [p.casefold() for p in candidate.parts[-len(parts):]]
517
+ if tail == parts:
518
+ matches.append(candidate)
519
+ return matches
520
+
521
+
522
+ def resolve(
523
+ project: str | Path,
524
+ search_root: str | Path | None = None,
525
+ ) -> Project:
526
+ """Resolve a project by path, by ``.mwt`` file, or by bare/qualified name.
527
+
528
+ Accepts an expanded directory, an ``.mwt`` file, or a name that is searched
529
+ for under ``search_root`` (default :data:`DEFAULT_SEARCH_ROOT`).
530
+ """
531
+ candidate = Path(project)
532
+ if candidate.exists():
533
+ resolved = candidate.resolve()
534
+ if resolved.is_file():
535
+ if resolved.suffix.lower() == ".mwt":
536
+ expanded = resolved.with_suffix("")
537
+ if expanded.is_dir():
538
+ return Project(root=expanded)
539
+ raise ProjectNotFound(
540
+ f"{resolved.name} has no expanded project directory beside it "
541
+ f"({expanded}). Open the project in MotionWorks once to expand it."
542
+ )
543
+ raise ProjectNotFound(
544
+ f"{resolved} is a file; expected an expanded project directory or .mwt"
545
+ )
546
+ if looks_like_project(resolved):
547
+ return Project(root=resolved)
548
+ raise ProjectNotFound(
549
+ f"{resolved} does not look like an expanded MotionWorks project "
550
+ f"(no POE directory or project markers)"
551
+ )
552
+
553
+ # Treat the argument as a name (possibly path-qualified) and search. Try the
554
+ # whole query first: a query containing a separator is itself a qualified
555
+ # suffix such as "MP2600iec Program/TopCutter", and matching on just the last
556
+ # component would wrongly discard the qualifier.
557
+ base = Path(search_root) if search_root else Path(DEFAULT_SEARCH_ROOT)
558
+ if not base.is_absolute():
559
+ base = Path.cwd() / base
560
+ raw = str(project)
561
+ matches = _match_by_name(base, raw)
562
+ if not matches and candidate.name and candidate.name != raw:
563
+ matches = _match_by_name(base, candidate.name)
564
+ if len(matches) == 1:
565
+ return Project(root=matches[0])
566
+ if len(matches) > 1:
567
+ listing = "\n ".join(str(m) for m in matches)
568
+ raise AmbiguousProject(
569
+ f"{project!r} matches {len(matches)} projects; qualify it with a "
570
+ f"parent directory or pass a full path:\n {listing}"
571
+ )
572
+ available = discover(base)
573
+ listing = "\n ".join(str(p) for p in available) or "<none found>"
574
+ raise ProjectNotFound(
575
+ f"No project named {project!r} under {base}. Available projects:\n {listing}"
576
+ )
577
+
578
+
579
+ def describe_all(search_root: str | Path | None = None) -> list[dict[str, object]]:
580
+ """Inventory every discovered project, for the list tool."""
581
+ base = Path(search_root) if search_root else Path(DEFAULT_SEARCH_ROOT)
582
+ if not base.is_absolute():
583
+ base = Path.cwd() / base
584
+ out: list[dict[str, object]] = []
585
+ for path in discover(base):
586
+ project = Project(root=path)
587
+ out.append(
588
+ {
589
+ "name": project.name,
590
+ "root": str(path),
591
+ "pou_count": len(project.pous()),
592
+ "languages": _language_counts(project),
593
+ "last_change": project.last_change(),
594
+ }
595
+ )
596
+ return out
597
+
598
+
599
+ def _language_counts(project: Project) -> dict[str, int]:
600
+ counts: dict[str, int] = {}
601
+ for pou in project.pous():
602
+ key = pou.language() or "?"
603
+ counts[key] = counts.get(key, 0) + 1
604
+ return counts
605
+
606
+
607
+ def library_usage(project: Project) -> dict[str, int]:
608
+ """Count how many POUs declare each function-block type.
609
+
610
+ Useful context for an agent: it shows which library blocks the project
611
+ actually depends on, and therefore which donor POUs are good examples.
612
+ """
613
+ counts: dict[str, int] = {}
614
+ for pou in project.pous():
615
+ try:
616
+ table = pou.declarations()
617
+ except UnsupportedFormat:
618
+ continue
619
+ for variable in table.variables:
620
+ if variable.is_fb_instance:
621
+ counts[variable.type_name] = counts.get(variable.type_name, 0) + 1
622
+ return dict(sorted(counts.items(), key=lambda kv: -kv[1]))
623
+
624
+
625
+ def symbol_index(project: Project) -> dict[str, list[str]]:
626
+ """Map every declared symbol name to the POU(s) that declare it.
627
+
628
+ This is the cross-reference an agent needs before editing code, so it can
629
+ avoid inventing symbol names that do not exist.
630
+ """
631
+ index: dict[str, list[str]] = {}
632
+
633
+ def record(name: str, where: str) -> None:
634
+ index.setdefault(name, [])
635
+ if where not in index[name]:
636
+ index[name].append(where)
637
+
638
+ try:
639
+ for variable in project.global_variables().variables:
640
+ record(variable.name, "<global>")
641
+ except UnsupportedFormat:
642
+ pass
643
+
644
+ for pou in project.pous():
645
+ try:
646
+ table = pou.declarations()
647
+ except UnsupportedFormat:
648
+ continue
649
+ for variable in table.variables:
650
+ record(variable.name, pou.name)
651
+ return index
652
+
653
+
654
+ def known_symbol_names(project: Project) -> set[str]:
655
+ """Every name that could legitimately appear in a POU body.
656
+
657
+ This is the allowance list for the ST linter: declared symbols from the
658
+ whole project plus every type name in use. It is intentionally generous.
659
+ Flagging a name that exists elsewhere would be a false positive, and a
660
+ linter that cries wolf gets ignored; a name that is genuinely undefined will
661
+ still be caught, because it appears nowhere at all.
662
+ """
663
+ names: set[str] = set(symbol_index(project).keys())
664
+ try:
665
+ names |= {v.type_name for v in project.global_variables().variables}
666
+ except UnsupportedFormat:
667
+ pass
668
+ for pou in project.pous():
669
+ try:
670
+ names |= {v.type_name for v in pou.declarations().variables}
671
+ except UnsupportedFormat:
672
+ continue
673
+ return names
674
+
675
+
676
+ def global_names(project: Project) -> set[str]:
677
+ """The names of the project's VAR_GLOBAL declarations.
678
+
679
+ Separate from ``known_symbol_names``, which is deliberately generous, because a
680
+ global is reachable from a POU ONLY if that POU declares it as VAR_EXTERNAL. The
681
+ linter needs to tell the two apart: a name that resolves nowhere is a spelling
682
+ mistake, while a global this POU has not imported is a build that STALLS with an
683
+ empty Errors pane. Measured with PLCMODE_ON, a system global the project has had
684
+ from the first byte.
685
+ """
686
+ try:
687
+ return {v.name for v in project.global_variables().variables}
688
+ except UnsupportedFormat:
689
+ return set()
690
+
691
+
692
+ def find_symbol(project: Project, symbol: str) -> list[Variable]:
693
+ """Return every declaration of ``symbol`` anywhere in the project."""
694
+ found: list[Variable] = []
695
+ try:
696
+ found.extend(project.global_variables().variants(symbol))
697
+ except UnsupportedFormat:
698
+ pass
699
+ for pou in project.pous():
700
+ try:
701
+ found.extend(pou.declarations().variants(symbol))
702
+ except UnsupportedFormat:
703
+ continue
704
+ return found