@aryacode/dsh-engineering 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/cordis.patch.yml +122 -0
  5. package/locale/en.json +6 -0
  6. package/locale/zh.json +6 -0
  7. package/package.json +77 -3
  8. package/vendor/ftview/README.md +55 -0
  9. package/vendor/ftview/SKILL.md +74 -0
  10. package/vendor/ftview/lib/checks.d.ts +27 -0
  11. package/vendor/ftview/lib/checks.js +39 -0
  12. package/vendor/ftview/lib/core.d.ts +208 -0
  13. package/vendor/ftview/lib/core.js +160 -0
  14. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  15. package/vendor/ftview/lib/edit/drafts.js +84 -0
  16. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  17. package/vendor/ftview/lib/edit/operations.js +126 -0
  18. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  19. package/vendor/ftview/lib/edit/prepare.js +133 -0
  20. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  21. package/vendor/ftview/lib/edit/tools.js +198 -0
  22. package/vendor/ftview/lib/edit.d.ts +30 -0
  23. package/vendor/ftview/lib/edit.js +38 -0
  24. package/vendor/ftview/lib/index.d.ts +31 -0
  25. package/vendor/ftview/lib/index.js +36 -0
  26. package/vendor/ftview/lib/model.d.ts +80 -0
  27. package/vendor/ftview/lib/model.js +267 -0
  28. package/vendor/ftview/lib/session.d.ts +48 -0
  29. package/vendor/ftview/lib/session.js +98 -0
  30. package/vendor/ftview/lib/skill.d.ts +29 -0
  31. package/vendor/ftview/lib/skill.js +43 -0
  32. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  33. package/vendor/ftview/lib/studio/discover.js +87 -0
  34. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  35. package/vendor/ftview/lib/studio/preflight.js +64 -0
  36. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  37. package/vendor/ftview/lib/studio/tools.js +83 -0
  38. package/vendor/ftview/lib/studio.d.ts +30 -0
  39. package/vendor/ftview/lib/studio.js +37 -0
  40. package/vendor/ftview/lib/tools.d.ts +14 -0
  41. package/vendor/ftview/lib/tools.js +202 -0
  42. package/vendor/l5x/README.md +62 -0
  43. package/vendor/l5x/SKILL.md +81 -0
  44. package/vendor/l5x/lib/approvals.d.ts +50 -0
  45. package/vendor/l5x/lib/approvals.js +93 -0
  46. package/vendor/l5x/lib/client.js +412 -0
  47. package/vendor/l5x/lib/client.js.map +1 -0
  48. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  49. package/vendor/l5x/lib/core/errors.js +48 -0
  50. package/vendor/l5x/lib/core/params.d.ts +49 -0
  51. package/vendor/l5x/lib/core/params.js +39 -0
  52. package/vendor/l5x/lib/core/result.d.ts +46 -0
  53. package/vendor/l5x/lib/core/result.js +21 -0
  54. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  55. package/vendor/l5x/lib/core/tool.js +19 -0
  56. package/vendor/l5x/lib/drafts.d.ts +66 -0
  57. package/vendor/l5x/lib/drafts.js +98 -0
  58. package/vendor/l5x/lib/editing.d.ts +30 -0
  59. package/vendor/l5x/lib/editing.js +42 -0
  60. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  61. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  62. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  63. package/vendor/l5x/lib/engine/analysis.js +258 -0
  64. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  65. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  66. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  67. package/vendor/l5x/lib/engine/change-set.js +446 -0
  68. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  69. package/vendor/l5x/lib/engine/configuration.js +42 -0
  70. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  71. package/vendor/l5x/lib/engine/details.js +173 -0
  72. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  73. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  74. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  75. package/vendor/l5x/lib/engine/download-review.js +63 -0
  76. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  77. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  78. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  79. package/vendor/l5x/lib/engine/observations.js +67 -0
  80. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  81. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  82. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  83. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  84. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  85. package/vendor/l5x/lib/engine/review.js +123 -0
  86. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  87. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  88. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  89. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  90. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  91. package/vendor/l5x/lib/engine/usage.js +92 -0
  92. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  93. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  94. package/vendor/l5x/lib/fs.d.ts +17 -0
  95. package/vendor/l5x/lib/fs.js +36 -0
  96. package/vendor/l5x/lib/index.d.ts +36 -0
  97. package/vendor/l5x/lib/index.js +42 -0
  98. package/vendor/l5x/lib/live.d.ts +48 -0
  99. package/vendor/l5x/lib/live.js +48 -0
  100. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  101. package/vendor/l5x/lib/model/aois.js +112 -0
  102. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  103. package/vendor/l5x/lib/model/errors.js +18 -0
  104. package/vendor/l5x/lib/model/index.d.ts +20 -0
  105. package/vendor/l5x/lib/model/index.js +19 -0
  106. package/vendor/l5x/lib/model/module.d.ts +37 -0
  107. package/vendor/l5x/lib/model/module.js +54 -0
  108. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  109. package/vendor/l5x/lib/model/modules.js +55 -0
  110. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  111. package/vendor/l5x/lib/model/parse.js +116 -0
  112. package/vendor/l5x/lib/model/project.d.ts +15 -0
  113. package/vendor/l5x/lib/model/project.js +31 -0
  114. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  115. package/vendor/l5x/lib/model/routines.js +183 -0
  116. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  117. package/vendor/l5x/lib/model/tags.js +119 -0
  118. package/vendor/l5x/lib/model/types.d.ts +304 -0
  119. package/vendor/l5x/lib/model/types.js +12 -0
  120. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  121. package/vendor/l5x/lib/model/udts.js +81 -0
  122. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  123. package/vendor/l5x/lib/model/xml.js +291 -0
  124. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  125. package/vendor/l5x/lib/model/xref.js +154 -0
  126. package/vendor/l5x/lib/session.d.ts +119 -0
  127. package/vendor/l5x/lib/session.js +236 -0
  128. package/vendor/l5x/lib/skill.d.ts +30 -0
  129. package/vendor/l5x/lib/skill.js +44 -0
  130. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  131. package/vendor/l5x/lib/studio-state.js +79 -0
  132. package/vendor/l5x/lib/studio.d.ts +29 -0
  133. package/vendor/l5x/lib/studio.js +40 -0
  134. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  135. package/vendor/l5x/lib/tools/checks.js +140 -0
  136. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  137. package/vendor/l5x/lib/tools/common.js +84 -0
  138. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  139. package/vendor/l5x/lib/tools/controller.js +170 -0
  140. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  141. package/vendor/l5x/lib/tools/drafts.js +270 -0
  142. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  143. package/vendor/l5x/lib/tools/live.js +175 -0
  144. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  145. package/vendor/l5x/lib/tools/read.js +245 -0
  146. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  147. package/vendor/l5x/lib/tools/references.js +269 -0
  148. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  149. package/vendor/l5x/lib/tools/studio.js +193 -0
  150. package/vendor/l5x/poll-worker.mjs +77 -0
  151. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  152. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  153. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  154. package/vendor/mwi/README.md +60 -0
  155. package/vendor/mwi/SKILL.md +87 -0
  156. package/vendor/mwi/bridge/README.md +8 -0
  157. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  158. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  159. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  160. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  161. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  162. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  163. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  164. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  165. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  229. package/vendor/mwi/code/mw_code.py +1258 -0
  230. package/vendor/mwi/legacy/edit-session.js +50 -0
  231. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  232. package/vendor/mwi/legacy/native-code.js +83 -0
  233. package/vendor/mwi/legacy/native-groups.js +75 -0
  234. package/vendor/mwi/legacy/native-structure.js +143 -0
  235. package/vendor/mwi/legacy/native-variables.js +77 -0
  236. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  237. package/vendor/mwi/legacy/pou-package.js +66 -0
  238. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  239. package/vendor/mwi/lib/core/bridge.js +53 -0
  240. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  241. package/vendor/mwi/lib/core/code-engine.js +109 -0
  242. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  243. package/vendor/mwi/lib/core/errors.js +47 -0
  244. package/vendor/mwi/lib/core/params.d.ts +52 -0
  245. package/vendor/mwi/lib/core/params.js +46 -0
  246. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  247. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  248. package/vendor/mwi/lib/core/result.d.ts +45 -0
  249. package/vendor/mwi/lib/core/result.js +20 -0
  250. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  251. package/vendor/mwi/lib/core/tool.js +55 -0
  252. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  253. package/vendor/mwi/lib/edit/compare.js +70 -0
  254. package/vendor/mwi/lib/edit.d.ts +20 -0
  255. package/vendor/mwi/lib/edit.js +32 -0
  256. package/vendor/mwi/lib/index.d.ts +21 -0
  257. package/vendor/mwi/lib/index.js +33 -0
  258. package/vendor/mwi/lib/read/service.d.ts +117 -0
  259. package/vendor/mwi/lib/read/service.js +229 -0
  260. package/vendor/mwi/lib/read.d.ts +21 -0
  261. package/vendor/mwi/lib/read.js +33 -0
  262. package/vendor/mwi/lib/session/project.d.ts +34 -0
  263. package/vendor/mwi/lib/session/project.js +54 -0
  264. package/vendor/mwi/lib/session/service.d.ts +167 -0
  265. package/vendor/mwi/lib/session/service.js +566 -0
  266. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  267. package/vendor/mwi/lib/session/stage.js +187 -0
  268. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  269. package/vendor/mwi/lib/session/workspace.js +90 -0
  270. package/vendor/mwi/lib/skill.d.ts +36 -0
  271. package/vendor/mwi/lib/skill.js +56 -0
  272. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  273. package/vendor/mwi/lib/tools/edit.js +286 -0
  274. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  275. package/vendor/mwi/lib/tools/read.js +161 -0
  276. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  277. package/vendor/mwi/lib/tools/session.js +166 -0
  278. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  279. package/vendor/mwi/lib/tools/verify.js +85 -0
  280. package/vendor/mwi/lib/verify.d.ts +17 -0
  281. package/vendor/mwi/lib/verify.js +29 -0
@@ -0,0 +1,292 @@
1
+ """EtherNet/IP assembly map: how much of each assembly a project actually uses.
2
+
3
+ WHY THIS EXISTS
4
+ ---------------
5
+ "Is there room for another status word?" is asked every time a diagnostic is added, and answering
6
+ it took several steps in the session that prompted this: the ``%QW`` base, the
7
+ ``I.Data[n] = %QW(base + 2n)`` rule, a scan of the project's declarations, and the module's
8
+ ``PrimCxnInputSize``/``PrimCxnOutputSize`` attributes in the L5X. Every one of those is a fact
9
+ that can be read, so the answer belongs in one call rather than in a reconstruction.
10
+
11
+ WHAT IT READS, AND FROM WHERE
12
+ -----------------------------
13
+ Two independent sources, reported separately because they can disagree:
14
+
15
+ * the **L5X** module definition gives the assembly SIZES, in bytes, as the scanner declares them
16
+ (``Communications/@PrimCxnInputSize`` and ``PrimCxnOutputSize``);
17
+ * the **MotionWorks project** gives the USED range, because every mapped word and bit is a
18
+ declared variable carrying an IEC address such as ``%QX21488.0`` or ``%QW21492``.
19
+
20
+ Neither alone answers the question. Sizes without usage cannot say how much is left; usage
21
+ without sizes cannot say what is left *of*.
22
+
23
+ ADDRESS ARITHMETIC
24
+ ------------------
25
+ The addresses are 16-bit word addresses, and ``.n`` names a bit. Measured on the TopCutter
26
+ interface: ``%IX21488.0`` is ``O.Data[0].0`` and ``%IW21490`` is ``O.Data[1]``, so a data word is
27
+ two address units. The byte width of a word is therefore what turns bytes into words:
28
+ ``words = bytes / 2``. The module and the project both use 16-bit ``INT`` elements.
29
+
30
+ THE ONE THING THIS DOES NOT DO
31
+ ------------------------------
32
+ It does not decide that a gap is usable. It reports the first word address that is NOT used and
33
+ the room remaining after it, which is arithmetic over what was read; whether an unused word is
34
+ free to take is a question about the peer's program, and guessing at that is how an interface
35
+ ends up with two things writing the same offset.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import re
41
+ import xml.etree.ElementTree as ET
42
+ from pathlib import Path
43
+
44
+ #: ``%QW21492``, ``%IX21488.0``, ``%IB21488`` — the IEC address forms this understands.
45
+ _ADDRESS = re.compile(
46
+ r"^%(?P<area>[IQM])(?P<size>[XBWDL])(?P<word>\d+)(?:\.(?P<bit>\d+))?$",
47
+ re.IGNORECASE,
48
+ )
49
+
50
+ #: Bits per word, and the byte width of one address unit, for a data word on this interface.
51
+ #: Measured, not assumed: O.Data[n] advances the address by 2, and each element is an INT.
52
+ ADDRESS_UNITS_PER_WORD = 2
53
+ BYTES_PER_WORD = 2
54
+
55
+
56
+ def read_project_addresses(project_root: Path | str) -> list[dict]:
57
+ """Every declared variable in the project that carries an IEC address.
58
+
59
+ Uses the same declaration reader the rest of the engine uses, so this cannot drift from what
60
+ mw_code_globals and mw_code_read_st report.
61
+ """
62
+ from . import project as P
63
+
64
+ root = Path(project_root)
65
+ proj = P.Project(root=root)
66
+ found: list[dict] = []
67
+
68
+ def collect(table, owner: str) -> None:
69
+ for v in getattr(table, "variables", []) or []:
70
+ address = getattr(v, "address", None)
71
+ if not address:
72
+ continue
73
+ found.append({
74
+ "name": getattr(v, "name", None),
75
+ "type": getattr(v, "type_name", None),
76
+ "address": str(address),
77
+ "owner": owner,
78
+ })
79
+
80
+ try:
81
+ collect(proj.global_variables(), "(globals)")
82
+ except Exception:
83
+ pass
84
+ for info in proj.pous():
85
+ try:
86
+ collect(info.declarations(), info.name)
87
+ except Exception:
88
+ continue
89
+ return found
90
+
91
+
92
+ def _data_words(addresses: list[dict]) -> dict:
93
+ """Group ``%I``/``%Q`` addresses into the used word set, per direction.
94
+
95
+ ``.`` marks a bit inside a word, so ``%IX21488.0`` and ``%IW21490`` both occupy a word: the
96
+ first shares its word with its neighbours, the second owns one. Both are recorded as used,
97
+ which is what "is there room" has to be computed against.
98
+ """
99
+ used: dict[str, set[int]] = {"input": set(), "output": set()}
100
+ detail: dict[str, list[dict]] = {"input": [], "output": []}
101
+ unparsed: list[str] = []
102
+
103
+ for entry in addresses:
104
+ m = _ADDRESS.match(entry["address"])
105
+ if not m:
106
+ unparsed.append(entry["address"])
107
+ continue
108
+ area = m.group("area").upper()
109
+ if area not in ("I", "Q"):
110
+ continue # %M (internal marker) is not part of an assembly
111
+ direction = "input" if area == "I" else "output"
112
+ word = int(m.group("word"))
113
+ size = m.group("size").upper()
114
+ bit = m.group("bit")
115
+ if size == "X" and bit is not None:
116
+ # A bit address names its word, and the word it names is the one in use.
117
+ used[direction].add(word)
118
+ detail[direction].append({**entry, "direction": direction, "word": word, "kind": "bit"})
119
+ elif size in ("W", "B", "D", "L"):
120
+ # A whole-element address. W is one word; the wider forms occupy more than one, and
121
+ # this reports the span rather than pretending they are all one word wide.
122
+ span = {"W": 1, "B": 1, "D": 2, "L": 2}[size]
123
+ # Address units: a word address advances by ADDRESS_UNITS_PER_WORD, so each
124
+ # extra data word advances the address by that much.
125
+ for i in range(span):
126
+ used[direction].add(word + i * ADDRESS_UNITS_PER_WORD)
127
+ detail[direction].append({
128
+ **entry, "direction": direction, "word": word, "kind": f"element({size})",
129
+ "words": span,
130
+ })
131
+ else:
132
+ unparsed.append(entry["address"])
133
+
134
+ return {"used": used, "detail": detail, "unparsed": unparsed}
135
+
136
+
137
+ def _runs(words: set[int]) -> list[dict]:
138
+ """Contiguous runs of used word addresses, so a gap is visible as a gap."""
139
+ if not words:
140
+ return []
141
+ ordered = sorted(words)
142
+ runs = []
143
+ start = prev = ordered[0]
144
+ for w in ordered[1:]:
145
+ if w == prev + ADDRESS_UNITS_PER_WORD:
146
+ prev = w
147
+ continue
148
+ runs.append({"first": start, "last": prev, "words": (prev - start) // ADDRESS_UNITS_PER_WORD + 1})
149
+ start = prev = w
150
+ runs.append({"first": start, "last": prev, "words": (prev - start) // ADDRESS_UNITS_PER_WORD + 1})
151
+ return runs
152
+
153
+
154
+ def read_l5x_module(l5x: Path | str, module_name: str | None = None) -> dict:
155
+ """The Ethernet module's declared assembly sizes, from an exported L5X.
156
+
157
+ Reads the ``<Module>`` whose ``CatalogNumber`` is ``ETHERNET-MODULE`` — a generic
158
+ EtherNet/IP module — because that is the shape this interface uses. A named module wins when
159
+ the caller passes one.
160
+ """
161
+ path = Path(l5x)
162
+ if not path.is_file():
163
+ raise FileNotFoundError(f"L5X not found: {path}")
164
+ tree = ET.parse(path)
165
+ root = tree.getroot()
166
+
167
+ candidates = []
168
+ for module in root.iter("Module"):
169
+ if module_name and module.get("Name") != module_name:
170
+ continue
171
+ if not module_name and module.get("CatalogNumber") != "ETHERNET-MODULE":
172
+ continue
173
+ comms = module.find("Communications")
174
+ if comms is None:
175
+ continue
176
+ candidates.append({
177
+ "name": module.get("Name"),
178
+ "catalog": module.get("CatalogNumber"),
179
+ "vendor": module.get("Vendor"),
180
+ "product_type": module.get("ProductType"),
181
+ "product_code": module.get("ProductCode"),
182
+ "revision": f"{module.get('Major')}.{module.get('Minor')}",
183
+ "input_bytes": int(comms.get("PrimCxnInputSize") or 0),
184
+ "output_bytes": int(comms.get("PrimCxnOutputSize") or 0),
185
+ })
186
+
187
+ if not candidates:
188
+ named = f" named '{module_name}'" if module_name else ""
189
+ raise ValueError(
190
+ f"no EtherNet/IP module{named} with a <Communications> element in {path.name}. "
191
+ "Pass module_name if the module is not a generic ETHERNET-MODULE."
192
+ )
193
+ return {"l5x": str(path), "modules": candidates}
194
+
195
+
196
+ def build_map(
197
+ project_root: Path | str | None = None,
198
+ l5x: Path | str | None = None,
199
+ module_name: str | None = None,
200
+ ) -> dict:
201
+ """Assemble the answer from whichever sources the caller supplied."""
202
+ report: dict = {"automatic_changes": False}
203
+
204
+ modules: list[dict] = []
205
+ if l5x:
206
+ report["module_source"] = read_l5x_module(l5x, module_name)
207
+ modules = report["module_source"]["modules"]
208
+
209
+ used = {"input": set(), "output": set()}
210
+ detail = {"input": [], "output": []}
211
+ unparsed: list[str] = []
212
+ if project_root:
213
+ parsed = _data_words(read_project_addresses(project_root))
214
+ used, detail, unparsed = parsed["used"], parsed["detail"], parsed["unparsed"]
215
+
216
+ assemblies: list[dict] = []
217
+ for direction, size_key in (("input", "input_bytes"), ("output", "output_bytes")):
218
+ entry: dict = {"direction": direction}
219
+ words = used[direction]
220
+ declared_words = None
221
+ if modules:
222
+ declared_words = modules[0][size_key] // BYTES_PER_WORD
223
+ entry["declared_bytes"] = modules[0][size_key]
224
+ entry["declared_words"] = declared_words
225
+
226
+ # THE ASSEMBLY IS A WINDOW, NOT EVERY ADDRESS IN THE PROJECT.
227
+ #
228
+ # Measured: the project carries %I/%Q addresses at 21488 AND at 61443, and a naive
229
+ # min/max over both reported a 64-word assembly spanning 39,955 words with the "next free"
230
+ # address at 61445. Both numbers were arithmetically faithful and completely wrong: they
231
+ # described two separate I/O areas as one.
232
+ #
233
+ # The base is the lowest address in the direction, which is what a mapped assembly starts
234
+ # at; anything at or beyond base + declared span is a different module's area and is
235
+ # reported separately rather than folded in.
236
+ outside: list[int] = []
237
+ if declared_words is not None and words:
238
+ base = min(words)
239
+ limit = base + declared_words * ADDRESS_UNITS_PER_WORD
240
+ inside = {w for w in words if base <= w < limit}
241
+ outside = sorted(w for w in words if w >= limit)
242
+ entry["base_word_address"] = base
243
+ entry["window_last_word_address"] = limit - ADDRESS_UNITS_PER_WORD
244
+ words = inside
245
+
246
+ entry["used_words"] = len(words)
247
+ entry["used_runs"] = _runs(words)
248
+ if words:
249
+ entry["first_word_address"] = min(words)
250
+ entry["last_word_address"] = max(words)
251
+ entry["span_words"] = (max(words) - min(words)) // ADDRESS_UNITS_PER_WORD + 1
252
+ if declared_words is not None:
253
+ entry["unused_words"] = declared_words - entry["used_words"]
254
+ entry["full"] = entry["unused_words"] <= 0
255
+ # THE ANSWER TO THE QUESTION, when it can be given: the first word address past the
256
+ # highest one in use. Whether the PEER leaves it alone is not knowable from here.
257
+ if words:
258
+ entry["next_free_word_address"] = max(words) + ADDRESS_UNITS_PER_WORD
259
+ entry["contiguous_room_after_last"] = (
260
+ entry["window_last_word_address"] - max(words)
261
+ ) // ADDRESS_UNITS_PER_WORD
262
+ if outside:
263
+ entry["addresses_outside_this_assembly"] = {
264
+ "count": len(outside),
265
+ "first": outside[0],
266
+ "note": (
267
+ "these %I/%Q addresses lie at or beyond this assembly's declared span, so they "
268
+ "belong to a different module's area; they are NOT part of this map and this "
269
+ "map's unused_words does not describe them"
270
+ ),
271
+ }
272
+ entry["elements"] = detail[direction]
273
+ assemblies.append(entry)
274
+
275
+ report["assemblies"] = assemblies
276
+ report["address_rules"] = {
277
+ "unit": "16-bit word",
278
+ "bytes_per_word": BYTES_PER_WORD,
279
+ "address_units_per_word": ADDRESS_UNITS_PER_WORD,
280
+ "note": (
281
+ "measured on this interface: O.Data[1] is %IW21490 and O.Data[0].0 is %IX21488.0, so "
282
+ "each data word advances the address by 2 and each element is one 16-bit INT"
283
+ ),
284
+ }
285
+ if unparsed:
286
+ report["addresses_not_understood"] = sorted(set(unparsed))
287
+ report["note"] = (
288
+ "used_words counts declared variables carrying an %I/%Q address; it does not prove the "
289
+ "peer program leaves the remaining words alone, so confirm a new offset against the "
290
+ "CompactLogix side before writing it"
291
+ )
292
+ return report
@@ -0,0 +1,36 @@
1
+ """Typed errors that surface as actionable messages in MCP tool results."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class MotionWorksError(Exception):
7
+ """Base class for every failure this package reports to an agent."""
8
+
9
+
10
+ class ProjectNotFound(MotionWorksError):
11
+ """The requested project could not be resolved to an expanded directory."""
12
+
13
+
14
+ class AmbiguousProject(MotionWorksError):
15
+ """A bare project name matched more than one project on disk."""
16
+
17
+
18
+ class IdeRunning(MotionWorksError):
19
+ """MotionWorks IEC is open, so writing would be unsafe.
20
+
21
+ The IDE caches project state in memory and rewrites whole files on save, so
22
+ an external edit made while it is open will either be silently discarded or
23
+ produce an inconsistent project.
24
+ """
25
+
26
+
27
+ class UnsupportedFormat(MotionWorksError):
28
+ """The on-disk format differs from the versions this package understands."""
29
+
30
+
31
+ class VerificationFailed(MotionWorksError):
32
+ """A post-write read-back did not match what was written."""
33
+
34
+
35
+ class NotFound(MotionWorksError):
36
+ """A named POU, variable, or file does not exist in the project."""
@@ -0,0 +1,208 @@
1
+ """Read bounded compiler networks with exact POU and worksheet identities.
2
+
3
+ No binary graphical decoding or writes. The caller must bind this cache to a
4
+ fresh native Build and unchanged complete source baseline.
5
+ """
6
+ import hashlib
7
+ import re
8
+ from pathlib import Path, PureWindowsPath
9
+
10
+ HEADER = re.compile(r'\A\(\*\s*T: (PROGRAM|FUNCTION_BLOCK|FUNCTION) ([A-Za-z_][A-Za-z_0-9]*)\s', re.S)
11
+ SYMBOL = re.compile(r'@IFBP (\d+)\.(\d+)(?![\d.])|@(RV|IV|IFB) (\d+)(?!\d)')
12
+ DECLARATION = re.compile(r'^([A-Za-z_][A-Za-z_0-9]*)\t(\d+)\t(VAR[A-Z_]*)\t([^\r\n]+)\r?\n([^;]*);', re.M)
13
+ # Generated private code temporaries are counted for completeness, never exposed
14
+ # as public pins. CamGenerator contains @T_Code_00 after its 140 source rows.
15
+ DEPENDENCY_DECLARATION = re.compile(r'^((?:[A-Za-z_][A-Za-z_0-9@]*|@T_Code_[0-9]+))\t(\d+)\t(VAR[A-Z_]*)\t([^\r\n]+)\r?\n([^;]*);', re.M)
16
+ MAX_BYTES = 2_000_000
17
+
18
+
19
+ def read_text(path, root):
20
+ if not path.resolve().is_relative_to(root.resolve()):
21
+ raise ValueError('Compiler artifact resolves outside project')
22
+ before = path.stat()
23
+ raw = path.read_bytes()
24
+ after = path.stat()
25
+ if (before.st_size,before.st_mtime_ns)!=(after.st_size,after.st_mtime_ns):
26
+ raise ValueError('Compiler artifact changed during read')
27
+ if len(raw) > MAX_BYTES or b'\0' in raw:
28
+ raise ValueError('Unsupported compiler text size/encoding')
29
+ return raw.decode('latin1'), dict(path=str(path), sha256=hashlib.sha256(raw).hexdigest(), bytes=len(raw), modified_ms=after.st_mtime_ns/1_000_000)
30
+
31
+
32
+ def identity(text):
33
+ match = HEADER.match(text)
34
+ if not match:
35
+ raise ValueError('Unknown compiler listing header')
36
+ return match.groups()
37
+
38
+
39
+ def symbols(text, saved=None):
40
+ result, rows, ordinals, names = {}, [], set(), set()
41
+ def bind(token, row):
42
+ if token in result and result[token] != row:
43
+ raise ValueError('Ambiguous compiler symbol')
44
+ result[token] = row
45
+ for match in DECLARATION.finditer(text):
46
+ name, ordinal, section, type_token, detail = match.groups()
47
+ if ordinal in ordinals or name.casefold() in names or int(ordinal) < 1:
48
+ raise ValueError('Ambiguous compiler declaration ordinal/name')
49
+ ordinals.add(ordinal); names.add(name.casefold())
50
+ row = dict(name=name, section=section)
51
+ rows.append((name, section))
52
+ explicit = re.findall(r'^@(RV|IV) (\d+)(?:\s|$)', detail, re.M)
53
+ if len(explicit) > 1:
54
+ raise ValueError('Ambiguous compiler declaration token')
55
+ if explicit:
56
+ kind, number = explicit[0]
57
+ bind('@' + kind + ' ' + number, row)
58
+ elif section in ('VAR', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR_TEMP'):
59
+ # Local operands use the declaration ordinal. FB instances use the
60
+ # same ordinal under IFB; the type ID remains raw compiler evidence.
61
+ if re.fullmatch(r'@FB:\d+\s*', type_token):
62
+ bind('@IFB ' + ordinal, row)
63
+ elif re.match(r'@TYP:\d+(?:\s|$)', type_token):
64
+ bind('@IV ' + ordinal, row)
65
+ if saved is not None:
66
+ if sorted(rows) != sorted((v.name, v.section) for v in saved):
67
+ raise ValueError('Compiler declarations differ from saved worksheet')
68
+ return result
69
+
70
+
71
+ def pin_bindings(code, declarations, saved, path, root):
72
+ """Bind only public pins from exact compiler FB dependencies, never UI order."""
73
+ needed = {m.group(1) for m in SYMBOL.finditer(code) if m.group(1) is not None}
74
+ saved_types = {v.name: v.type_name for v in saved}
75
+ pins, artifacts, cache = {}, [], {}
76
+ directions = {'VAR_INPUT': 'input', 'VAR_OUTPUT': 'output', 'VAR_IN_OUT': 'in_out'}
77
+ for match in DECLARATION.finditer(declarations):
78
+ name, ordinal, section, type_token, _ = match.groups()
79
+ fb = re.fullmatch(r'@FB:(\d+)\s*', type_token)
80
+ if ordinal not in needed or not fb or section not in ('VAR', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR_TEMP'):
81
+ continue
82
+ type_id = int(fb.group(1))
83
+ if type_id not in cache:
84
+ dependency = path.with_name(f'ICI{type_id:05d}.DIT')
85
+ text, evidence = read_text(dependency, root)
86
+ kind, block = identity(text)
87
+ if '*)' not in text:
88
+ raise ValueError('Truncated compiler FB dependency header')
89
+ header = text.split('*)', 1)[0]
90
+ ids = re.findall(r'^CI#:\s*(\d+)\s*$', header, re.M)
91
+ counts = re.findall(r'^QVE:\s*(\d+)\s*$', header, re.M)
92
+ if kind != 'FUNCTION_BLOCK' or len(ids) != 1 or int(ids[0]) != type_id:
93
+ raise ValueError('Compiler FB dependency identity mismatch')
94
+ # Graphical library FBs contain compiler-only names such as Code@@80.
95
+ # Count those private rows, but never expose them as public pins.
96
+ rows = list(DEPENDENCY_DECLARATION.finditer(text))
97
+ # QVE includes internal declarations; a partial parse must not resolve pins.
98
+ if len(counts) != 1 or int(counts[0]) != len(rows):
99
+ raise ValueError('Compiler FB dependency declarations are incomplete')
100
+ symbols(text) # Reject duplicate names/ordinals and ambiguous bindings.
101
+ names = {r.group(1).casefold() for r in rows}
102
+ ordinals = {int(r.group(2)) for r in rows}
103
+ if len(names) != len(rows) or ordinals != set(range(1, len(rows)+1)):
104
+ raise ValueError('Compiler FB dependency ordinals are incomplete')
105
+ cache[type_id] = (block, rows, evidence)
106
+ artifacts.append(evidence)
107
+ block, rows, evidence = cache[type_id]
108
+ if saved_types[name].casefold() != block.casefold():
109
+ raise ValueError('Compiler FB dependency type differs from saved instance')
110
+ for row in rows:
111
+ pin_name, pin_ordinal, pin_section, _, _ = row.groups()
112
+ if pin_section in directions:
113
+ if not re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', pin_name):
114
+ raise ValueError('Compiler FB public pin name is unsupported')
115
+ pins[f'@IFBP {ordinal}.{int(pin_ordinal)}'] = dict(
116
+ declaration=dict(name=pin_name, section=pin_section),
117
+ pin_direction=directions[pin_section], block_type=block,
118
+ compiler_type_id=type_id, dependency_sha256=evidence['sha256'])
119
+ return pins, artifacts
120
+
121
+
122
+ def networks(text, bindings, pins=None):
123
+ result, current = [], None
124
+ if '*)' not in text:
125
+ raise ValueError('Truncated compiler header')
126
+ for line in text.split('*)', 1)[1].splitlines():
127
+ if line.strip() == '@NETWORK_BEGIN':
128
+ if current is not None:
129
+ raise ValueError('Nested compiler network')
130
+ current = []
131
+ elif line.strip() == '@NETWORK_END':
132
+ if current is None:
133
+ raise ValueError('Unmatched compiler network end')
134
+ result.append(dict(number=len(result)+1, lines=current))
135
+ current = None
136
+ elif line.strip():
137
+ if current is None:
138
+ raise ValueError('Instruction outside graphical network')
139
+ refs = []
140
+ for match in SYMBOL.finditer(line):
141
+ token = match.group()
142
+ if match.group(1) is not None:
143
+ instance = bindings.get('@IFB ' + match.group(1))
144
+ pin = (pins or {}).get(token) if instance else None
145
+ refs.append(dict(dict(token=token, declaration=None, resolved=pin is not None,
146
+ instance=instance, pin_ordinal=int(match.group(2))), **(pin or {})))
147
+ else:
148
+ refs.append(dict(token=token, declaration=bindings.get(token), resolved=token in bindings))
149
+ current.append(dict(raw=line, symbols=refs))
150
+ if current is not None:
151
+ raise ValueError('Truncated compiler network')
152
+ return result
153
+
154
+
155
+ def inspect(project, name, start=1, limit=10, *, source_only=False):
156
+ if type(start) is not int or start < 1 or type(limit) is not int or not 1 <= limit <= 50:
157
+ raise ValueError('Invalid network range')
158
+ pou = project.pou(name)
159
+ if pou.name != name or pou.body_stream()[1] not in (('ST', 'LD', 'FBD') if source_only else ('LD', 'FBD')):
160
+ raise ValueError('Exact graphical POU required')
161
+ root = project.root
162
+ candidates = []
163
+ for path in sorted((root / 'C').glob('*/R/*/ICI*.CIC')):
164
+ text, evidence = read_text(path, root)
165
+ if identity(text)[1] == name:
166
+ candidates.append((path, text, evidence))
167
+ if len(candidates) != 1:
168
+ raise ValueError('Compiler listing for exact POU absent or ambiguous; unused POUs may be omitted. Inspect usage without automatic task assignment')
169
+ path, code, evidence = candidates[0]
170
+ declarations, dit_evidence = read_text(path.with_suffix('.DIT'), root)
171
+ if identity(declarations) != identity(code):
172
+ raise ValueError('Compiler declaration identity mismatch')
173
+ worksheet, diw_evidence = read_text(path.with_suffix('.DIW'), root)
174
+ # Match the saved body worksheet, not a guessed filename or a sibling cache.
175
+ expected = 'poe/' + name.casefold() + '/' + pou.body_stream()[0].casefold()
176
+ rows = [line.split('\t') for line in worksheet.splitlines() if re.match(r'^\d+\t\d+\t', line)]
177
+ if len([r for r in rows if len(r)==3 and r[2].replace('\\','/').casefold()==expected]) != 1:
178
+ raise ValueError('Compiler worksheet identity mismatch')
179
+ mapping, sp_evidence = read_text(path.with_suffix('.SP'), root)
180
+ paths = [line.split('\t')[0] for line in mapping.splitlines() if line.strip() and not line.startswith('*\t')]
181
+ expected_file = str(root / Path(expected))
182
+ # Compiler Windows paths are compared as paths even in portable unit tests.
183
+ if paths and any(PureWindowsPath(p) != PureWindowsPath(expected_file) for p in paths):
184
+ raise ValueError('Compiler source mapping points to another project/worksheet')
185
+ table = pou.declarations()
186
+ if table.warnings:
187
+ raise ValueError('Saved declaration worksheet is incomplete')
188
+ bindings = symbols(declarations, table.variables)
189
+ pins, dependency_artifacts = ({}, []) if source_only else pin_bindings(code, declarations, table.variables, path, root)
190
+ parsed = [] if source_only else networks(code, bindings, pins)
191
+ if source_only and not paths:
192
+ raise ValueError('Compiled source has no worksheet mapping')
193
+ if parsed and not paths:
194
+ raise ValueError('Populated listing has no source mapping')
195
+ selected = parsed[start-1:start-1+limit]
196
+ artifacts=[evidence,dit_evidence,diw_evidence,sp_evidence]
197
+ for artifact in artifacts + dependency_artifacts:
198
+ if read_text(Path(artifact['path']),root)[1]!=artifact:
199
+ raise ValueError('Compiler artifact set changed during inspection')
200
+ if source_only:
201
+ return dict(pou=name, language=pou.body_stream()[1], artifacts=artifacts,
202
+ action_performed=False, compiler_cache_freshness_verified=False)
203
+ return dict(pou=name, language=pou.body_stream()[1], artifacts=artifacts,
204
+ network_count=len(parsed), start=start, networks=selected, has_more=start-1+len(selected)<len(parsed),
205
+ symbol_bindings=bindings, dependency_artifacts=dependency_artifacts,
206
+ compiler_cache_freshness_verified=False, compiler_dependency_freshness_verified=False,
207
+ action_performed=False, limitations=['Compiler tokens are retained verbatim; unresolved tokens have no inferred meaning.',
208
+ 'Compiler instructions do not prove canvas geometry, pin placement or runtime behavior.'])