@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.
- package/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/cordis.patch.yml +122 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/** Validate and stage coordinated edits while retaining untouched XML bytes. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { loadL5x } from "../model/index.js";
|
|
4
|
+
import { parseLadder, lexLogic } from "./logic-syntax.js";
|
|
5
|
+
import { compareProjects, runChecks } from "./review.js";
|
|
6
|
+
import { EngineeringError } from "../session.js";
|
|
7
|
+
import { typeInstances, typeIndex, udtDetail } from "./details.js";
|
|
8
|
+
import { uniqueRegion, xmlRegions } from "./xml-regions.js";
|
|
9
|
+
/** Scalar types the plugin can create with a literal initial value. */
|
|
10
|
+
const SCALAR_TYPES = new Set(['BOOL', 'SINT', 'INT', 'DINT', 'LINT', 'USINT', 'UINT', 'UDINT', 'ULINT', 'REAL', 'LREAL']);
|
|
11
|
+
/** Operation names accepted by a version-1 change set. */
|
|
12
|
+
const SUPPORTED_OPERATIONS = ['replace_rung', 'insert_rung', 'delete_rung', 'replace_st', 'add_routine', 'add_tag', 'set_tag_description', 'add_udt_member', 'set_udt_description', 'set_udt_member_description'];
|
|
13
|
+
/** Compact contract returned with validation errors and embedded in tool descriptions. */
|
|
14
|
+
export const CHANGE_SET_HELP = `Operations (each also needs "operation"):
|
|
15
|
+
replace_rung {program,routine,rung,text}
|
|
16
|
+
insert_rung {program,routine,rung,text,before_rung?}
|
|
17
|
+
delete_rung {program,routine,rung}
|
|
18
|
+
replace_st {program,routine,code}
|
|
19
|
+
add_routine {program,name,language:"RLL"|"ST",code}
|
|
20
|
+
add_tag {scope,name,data_type,description,value? scalar, dimensions? "4 2", external_access? "Read/Write"|"Read Only"|"None"}
|
|
21
|
+
set_tag_description {scope,name,description}
|
|
22
|
+
add_udt_member {udt,name,data_type,description}
|
|
23
|
+
set_udt_description {udt,description}
|
|
24
|
+
set_udt_member_description {udt,member,description}
|
|
25
|
+
Identifiers are at most 40 characters. Example: {"version":1,"operations":[{"operation":"add_tag","scope":"controller","name":"SP_Start","data_type":"REAL","value":80,"description":"Start threshold"}]}`;
|
|
26
|
+
const UDT_OPERATIONS = ['add_udt_member', 'set_udt_description', 'set_udt_member_description'];
|
|
27
|
+
const isUdtOperation = (operation) => UDT_OPERATIONS.includes(operation.operation);
|
|
28
|
+
/** Parse a Studio `Dimensions` value: one to three positive sizes. */
|
|
29
|
+
function parseDimensions(row) {
|
|
30
|
+
const value = row.dimensions;
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (typeof value !== 'string' || !/^[1-9]\d*(?: [1-9]\d*){0,2}$/u.test(value))
|
|
34
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'dimensions must be one to three space-separated positive integers');
|
|
35
|
+
const sizes = value.split(' ').map(part => Number.parseInt(part, 10));
|
|
36
|
+
if (sizes.some(size => size > 1_000_000) || sizes.reduce((product, size) => product * size, 1) > 10_000_000)
|
|
37
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'dimensions exceed the supported element count');
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function object(value) {
|
|
41
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
42
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'Expected an object');
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function text(row, name) {
|
|
46
|
+
if (typeof row[name] !== 'string')
|
|
47
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `${name} must be a string`);
|
|
48
|
+
return row[name];
|
|
49
|
+
}
|
|
50
|
+
function identifier(row, name) {
|
|
51
|
+
const value = text(row, name);
|
|
52
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,39}$/u.test(value))
|
|
53
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `${name} must be a Logix identifier of at most 40 characters`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function integer(row, name) {
|
|
57
|
+
const value = row[name];
|
|
58
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
59
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `${name} must be a nonnegative safe integer`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function fields(row, allowed) {
|
|
63
|
+
const extra = Object.keys(row).filter(name => !allowed.includes(name));
|
|
64
|
+
if (extra.length > 0)
|
|
65
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `Unsupported field(s): ${extra.join(', ')}. Allowed: ${allowed.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
/** Whether one literal fits the scalar type's range and integer-ness. */
|
|
68
|
+
function fitsScalar(type, value) {
|
|
69
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
70
|
+
return false;
|
|
71
|
+
switch (type.toUpperCase()) {
|
|
72
|
+
case 'BOOL': return value === 0 || value === 1;
|
|
73
|
+
case 'SINT': return Number.isInteger(value) && value >= -128 && value <= 127;
|
|
74
|
+
case 'INT': return Number.isInteger(value) && value >= -32768 && value <= 32767;
|
|
75
|
+
case 'DINT': return Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;
|
|
76
|
+
case 'USINT': return Number.isInteger(value) && value >= 0 && value <= 255;
|
|
77
|
+
case 'UINT': return Number.isInteger(value) && value >= 0 && value <= 65535;
|
|
78
|
+
case 'UDINT': return Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
79
|
+
case 'LINT': return Number.isSafeInteger(value);
|
|
80
|
+
case 'ULINT': return Number.isSafeInteger(value) && value >= 0;
|
|
81
|
+
case 'REAL': return Math.abs(value) <= 3.4028234663852886e38;
|
|
82
|
+
case 'LREAL': return Math.abs(value) <= Number.MAX_VALUE;
|
|
83
|
+
default: return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function externalAccess(row) {
|
|
87
|
+
const value = row.external_access;
|
|
88
|
+
if (value === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (typeof value !== 'string')
|
|
91
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'external_access must be "Read/Write", "Read Only" or "None"');
|
|
92
|
+
const normalized = value.trim().toLowerCase().replace(/[\s/]+/gu, '/');
|
|
93
|
+
if (normalized === 'read/write')
|
|
94
|
+
return 'Read/Write';
|
|
95
|
+
if (normalized === 'read/only')
|
|
96
|
+
return 'Read Only';
|
|
97
|
+
if (normalized === 'none')
|
|
98
|
+
return 'None';
|
|
99
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'external_access must be "Read/Write", "Read Only" or "None"');
|
|
100
|
+
}
|
|
101
|
+
function parseOne(value, declaredTypes) {
|
|
102
|
+
const row = object(value);
|
|
103
|
+
switch (row.operation) {
|
|
104
|
+
case 'replace_rung':
|
|
105
|
+
case 'insert_rung':
|
|
106
|
+
case 'delete_rung': {
|
|
107
|
+
fields(row, row.operation === 'delete_rung' ? ['operation', 'program', 'routine', 'rung'] : row.operation === 'insert_rung' ? ['operation', 'program', 'routine', 'rung', 'text', 'before_rung'] : ['operation', 'program', 'routine', 'rung', 'text']);
|
|
108
|
+
const selected = { program: identifier(row, 'program'), routine: identifier(row, 'routine'), rung: integer(row, 'rung') };
|
|
109
|
+
if (row.operation === 'delete_rung')
|
|
110
|
+
return { operation: row.operation, ...selected };
|
|
111
|
+
const source = text(row, 'text');
|
|
112
|
+
if (parseLadder(source).issues.length > 0)
|
|
113
|
+
throw new EngineeringError('INVALID_LADDER', 'Rung contains malformed or unsupported NeutralText structure');
|
|
114
|
+
if (row.operation === 'replace_rung')
|
|
115
|
+
return { operation: row.operation, ...selected, text: source };
|
|
116
|
+
return { operation: row.operation, ...selected, text: source, ...(row.before_rung === undefined ? {} : { before_rung: integer(row, 'before_rung') }) };
|
|
117
|
+
}
|
|
118
|
+
case 'replace_st':
|
|
119
|
+
fields(row, ['operation', 'program', 'routine', 'code']);
|
|
120
|
+
return { operation: row.operation, program: identifier(row, 'program'), routine: identifier(row, 'routine'), code: text(row, 'code') };
|
|
121
|
+
case 'add_routine': {
|
|
122
|
+
fields(row, ['operation', 'program', 'name', 'language', 'code']);
|
|
123
|
+
if (row.language !== 'RLL' && row.language !== 'ST')
|
|
124
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'language must be "RLL" or "ST"');
|
|
125
|
+
return { operation: row.operation, program: identifier(row, 'program'), name: identifier(row, 'name'), language: row.language, code: text(row, 'code') };
|
|
126
|
+
}
|
|
127
|
+
case 'add_tag': {
|
|
128
|
+
fields(row, ['operation', 'scope', 'name', 'data_type', 'value', 'dimensions', 'external_access', 'description']);
|
|
129
|
+
const dataType = text(row, 'data_type');
|
|
130
|
+
if (!declaredTypes.has(dataType.toLowerCase()))
|
|
131
|
+
throw new EngineeringError('UNKNOWN_DATA_TYPE', `Data type "${dataType}" is not a scalar or declared type in this export; call get_component kind="udt" or project_overview to check names`);
|
|
132
|
+
const scalar = SCALAR_TYPES.has(dataType.toUpperCase());
|
|
133
|
+
const dimensions = parseDimensions(row);
|
|
134
|
+
const access = externalAccess(row);
|
|
135
|
+
if (!scalar && row.value !== undefined)
|
|
136
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'A declared-type tag takes Studio default values; omit value');
|
|
137
|
+
if (dimensions !== undefined && row.value !== undefined)
|
|
138
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'An array tag takes Studio default element values; omit value');
|
|
139
|
+
if (dimensions === undefined && scalar) {
|
|
140
|
+
if (row.value === undefined)
|
|
141
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'value is required for a scalar tag; omit value for an array or declared-type tag');
|
|
142
|
+
if (!fitsScalar(dataType, row.value))
|
|
143
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `value ${String(row.value)} does not fit ${dataType}; BOOL accepts 0/1 and integer types accept their signed/unsigned range`);
|
|
144
|
+
return { operation: row.operation, scope: identifier(row, 'scope'), name: identifier(row, 'name'), data_type: dataType, value: row.value, description: text(row, 'description'), ...(access === undefined ? {} : { external_access: access }) };
|
|
145
|
+
}
|
|
146
|
+
return { operation: row.operation, scope: identifier(row, 'scope'), name: identifier(row, 'name'), data_type: dataType, ...(dimensions === undefined ? {} : { dimensions }), description: text(row, 'description'), ...(access === undefined ? {} : { external_access: access }) };
|
|
147
|
+
}
|
|
148
|
+
case 'add_udt_member': {
|
|
149
|
+
fields(row, ['operation', 'udt', 'name', 'data_type', 'description']);
|
|
150
|
+
const dataType = text(row, 'data_type');
|
|
151
|
+
if (dataType.toUpperCase() === 'BIT')
|
|
152
|
+
throw new EngineeringError('UNSUPPORTED_OPERATION', 'A BIT member needs a backing member and bit position; append a BOOL, numeric or declared-type member instead');
|
|
153
|
+
if (!declaredTypes.has(dataType.toLowerCase()))
|
|
154
|
+
throw new EngineeringError('UNKNOWN_DATA_TYPE', `Data type "${dataType}" is not a scalar or declared type in this export`);
|
|
155
|
+
return { operation: row.operation, udt: identifier(row, 'udt'), name: identifier(row, 'name'), data_type: dataType, description: text(row, 'description') };
|
|
156
|
+
}
|
|
157
|
+
case 'set_udt_description':
|
|
158
|
+
fields(row, ['operation', 'udt', 'description']);
|
|
159
|
+
return { operation: row.operation, udt: identifier(row, 'udt'), description: text(row, 'description') };
|
|
160
|
+
case 'set_udt_member_description':
|
|
161
|
+
fields(row, ['operation', 'udt', 'member', 'description']);
|
|
162
|
+
return { operation: row.operation, udt: identifier(row, 'udt'), member: identifier(row, 'member'), description: text(row, 'description') };
|
|
163
|
+
case 'set_tag_description':
|
|
164
|
+
fields(row, ['operation', 'scope', 'name', 'description']);
|
|
165
|
+
return { operation: row.operation, scope: identifier(row, 'scope'), name: identifier(row, 'name'), description: text(row, 'description') };
|
|
166
|
+
default: throw new EngineeringError('INVALID_CHANGE_SET', `Unsupported operation "${String(row.operation)}"; supported: ${SUPPORTED_OPERATIONS.join(', ')}. Arbitrary XML replacement is not accepted.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function parseOperations(request, maximum, declaredTypes) {
|
|
170
|
+
let value;
|
|
171
|
+
try {
|
|
172
|
+
value = JSON.parse(request);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `Invalid JSON: ${error instanceof Error ? error.message : String(error)}. ${CHANGE_SET_HELP}`);
|
|
176
|
+
}
|
|
177
|
+
const root = object(value);
|
|
178
|
+
fields(root, ['version', 'operations']);
|
|
179
|
+
if (root.version !== 1 || !Array.isArray(root.operations) || root.operations.length < 1 || root.operations.length > maximum)
|
|
180
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `version must be 1 with between 1 and ${maximum} operations. ${CHANGE_SET_HELP}`);
|
|
181
|
+
const operations = [];
|
|
182
|
+
const errors = [];
|
|
183
|
+
for (const [index, entry] of root.operations.entries()) {
|
|
184
|
+
try {
|
|
185
|
+
operations.push(parseOne(entry, declaredTypes));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const code = error instanceof EngineeringError ? error.code : 'INVALID_CHANGE_SET';
|
|
189
|
+
const message = error instanceof Error ? error.message.replace(/^[A-Z][A-Z_]*: /u, '') : String(error);
|
|
190
|
+
errors.push(`#${index} [${code}]: ${message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (errors.length > 0)
|
|
194
|
+
throw new EngineeringError('INVALID_CHANGE_SET', `${errors.length} of ${root.operations.length} operation(s) failed:\n${errors.map(line => `- ${line}`).join('\n')}\n${CHANGE_SET_HELP}`);
|
|
195
|
+
return operations;
|
|
196
|
+
}
|
|
197
|
+
const escape = (value) => value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
198
|
+
const cdata = (value) => `<![CDATA[${value.replaceAll(']]>', ']]]]><![CDATA[>')}]]>`;
|
|
199
|
+
const replace = (xml, start, end, value) => xml.slice(0, start) + value + xml.slice(end);
|
|
200
|
+
function editable(region) {
|
|
201
|
+
const protectedRegion = (region) => region.kind === 'EncodedData' || /safety/iu.test(region.attributes.Class ?? '') || Object.entries(region.attributes).some(([name, value]) => /Encrypted|SourceProtected/iu.test(name) && value.toLowerCase() !== 'false') || region.children.some(protectedRegion);
|
|
202
|
+
if (protectedRegion(region))
|
|
203
|
+
throw new EngineeringError('PROTECTED_TARGET', 'Safety or protected target cannot be changed');
|
|
204
|
+
}
|
|
205
|
+
function insert(xml, region, child) {
|
|
206
|
+
if (!region.selfClosing)
|
|
207
|
+
return replace(xml, region.contentEnd, region.contentEnd, child);
|
|
208
|
+
return replace(xml, region.start, region.end, xml.slice(region.start, region.end).replace(/\/\s*>$/u, '>') + child + `</${region.kind}>`);
|
|
209
|
+
}
|
|
210
|
+
function stContent(code) {
|
|
211
|
+
if (lexLogic(code).issues.length > 0)
|
|
212
|
+
throw new EngineeringError('INVALID_ST', 'Structured Text contains unterminated comments or strings');
|
|
213
|
+
return '<STContent>' + code.split('\n').map((line, index) => `<Line Number="${index}">${cdata(line)}</Line>`).join('') + '</STContent>';
|
|
214
|
+
}
|
|
215
|
+
/** Replace a description attribute or element inside one region. */
|
|
216
|
+
function setDescription(xml, region, value) {
|
|
217
|
+
const element = `<Description>${escape(value)}</Description>`;
|
|
218
|
+
const open = xml.slice(region.start, region.contentStart);
|
|
219
|
+
if (/\sDescription="/u.test(open))
|
|
220
|
+
return replace(xml, region.start, region.contentStart, open.replace(/(\sDescription=")[^"]*(")/u, (_match, start, end) => `${start}${escape(value)}${end}`));
|
|
221
|
+
const child = region.children.find(entry => entry.kind === 'Description');
|
|
222
|
+
if (child !== undefined)
|
|
223
|
+
return replace(xml, child.start, child.end, element);
|
|
224
|
+
return insert(xml, region, element);
|
|
225
|
+
}
|
|
226
|
+
function applyOperation(xml, operation, context) {
|
|
227
|
+
const root = uniqueRegion(xmlRegions(xml), 'RSLogix5000Content');
|
|
228
|
+
const controller = uniqueRegion(root.children, 'Controller');
|
|
229
|
+
const program = (name) => {
|
|
230
|
+
const found = uniqueRegion(uniqueRegion(controller.children, 'Programs').children, 'Program', name);
|
|
231
|
+
if (/safety/iu.test(found.attributes.Class ?? '') || /^safety$/iu.test(name))
|
|
232
|
+
throw new EngineeringError('PROTECTED_TARGET', 'Safety programs cannot be changed');
|
|
233
|
+
const tasks = controller.children.find(child => child.kind === 'Tasks');
|
|
234
|
+
if (tasks?.children.some(task => /safety/iu.test(`${task.attributes.Type ?? ''} ${task.attributes.Class ?? ''}`) && task.children.some(child => child.kind === 'ScheduledPrograms' && child.children.some(program => program.attributes.Name?.toLowerCase() === name.toLowerCase()))))
|
|
235
|
+
throw new EngineeringError('PROTECTED_TARGET', 'Safety-scheduled programs cannot be changed');
|
|
236
|
+
return found;
|
|
237
|
+
};
|
|
238
|
+
if (operation.operation === 'add_udt_member' || operation.operation === 'set_udt_description' || operation.operation === 'set_udt_member_description') {
|
|
239
|
+
const dataTypes = uniqueRegion(controller.children, 'DataTypes');
|
|
240
|
+
const type = uniqueRegion(dataTypes.children, 'DataType', operation.udt);
|
|
241
|
+
if (type.attributes.Family !== 'NoFamily')
|
|
242
|
+
throw new EngineeringError('PROTECTED_TARGET', 'Module-defined and built-in data types are vendor-owned and cannot be changed');
|
|
243
|
+
if (operation.operation === 'set_udt_description')
|
|
244
|
+
return setDescription(xml, type, operation.description);
|
|
245
|
+
const members = uniqueRegion(type.children, 'Members');
|
|
246
|
+
if (operation.operation === 'set_udt_member_description') {
|
|
247
|
+
const member = uniqueRegion(members.children, 'Member', operation.member);
|
|
248
|
+
return setDescription(xml, member, operation.description);
|
|
249
|
+
}
|
|
250
|
+
const instances = typeInstances(context.document, operation.udt);
|
|
251
|
+
if (context.allowUdtInstances !== true && instances.length > 0)
|
|
252
|
+
throw new EngineeringError('UDT_HAS_INSTANCES', `Data type ${operation.udt} has ${instances.length} instance(s) (first: ${instances[0]}); changing a member rewrites serialized tag data. Use propose_udt_change to emit a Data Type component for Studio import.`);
|
|
253
|
+
if (members.children.some(member => member.attributes.Name?.toLowerCase() === operation.name.toLowerCase()))
|
|
254
|
+
throw new EngineeringError('NAME_COLLISION', 'Member already exists in the data type');
|
|
255
|
+
const dataType = operation.data_type.toUpperCase();
|
|
256
|
+
const scalar = SCALAR_TYPES.has(dataType);
|
|
257
|
+
const radix = scalar ? (dataType === 'REAL' || dataType === 'LREAL' ? 'Float' : 'Decimal') : 'NullType';
|
|
258
|
+
const description = operation.description === '' ? '' : ` Description="${escape(operation.description)}"`;
|
|
259
|
+
return insert(xml, members, `<Member Name="${operation.name}" DataType="${operation.data_type}" Dimension="0" Radix="${radix}" Hidden="false" ExternalAccess="Read/Write"${description}/>`);
|
|
260
|
+
}
|
|
261
|
+
if (operation.operation === 'add_tag' || operation.operation === 'set_tag_description') {
|
|
262
|
+
const owner = operation.scope.toLowerCase() === 'controller' ? controller : program(operation.scope);
|
|
263
|
+
const tags = uniqueRegion(owner.children, 'Tags');
|
|
264
|
+
if (operation.operation === 'add_tag') {
|
|
265
|
+
if (tags.children.some(tag => tag.attributes.Name?.toLowerCase() === operation.name.toLowerCase()))
|
|
266
|
+
throw new EngineeringError('NAME_COLLISION', 'Tag already exists in the selected scope');
|
|
267
|
+
if (operation.dimensions === undefined && operation.value !== undefined) {
|
|
268
|
+
const float = operation.data_type.toUpperCase() === 'REAL' || operation.data_type.toUpperCase() === 'LREAL';
|
|
269
|
+
return insert(xml, tags, `<Tag Name="${operation.name}" TagType="Base" DataType="${operation.data_type}" Radix="${float ? 'Float' : 'Decimal'}" Constant="false" ExternalAccess="${operation.external_access ?? 'Read/Write'}"><Description>${escape(operation.description)}</Description><Data Format="L5K">${cdata(String(operation.value))}</Data></Tag>`);
|
|
270
|
+
}
|
|
271
|
+
const scalar = SCALAR_TYPES.has(operation.data_type.toUpperCase());
|
|
272
|
+
const radix = scalar ? ` Radix="${operation.data_type.toUpperCase() === 'REAL' || operation.data_type.toUpperCase() === 'LREAL' ? 'Float' : 'Decimal'}"` : '';
|
|
273
|
+
const dimensions = operation.dimensions === undefined ? '' : ` Dimensions="${operation.dimensions}"`;
|
|
274
|
+
return insert(xml, tags, `<Tag Name="${operation.name}" TagType="Base" DataType="${operation.data_type}"${dimensions}${radix} Constant="false" ExternalAccess="${operation.external_access ?? 'Read/Write'}"><Description>${escape(operation.description)}</Description></Tag>`);
|
|
275
|
+
}
|
|
276
|
+
const tag = uniqueRegion(tags.children, 'Tag', operation.name);
|
|
277
|
+
editable(tag);
|
|
278
|
+
if (tag.attributes.Description !== undefined)
|
|
279
|
+
throw new EngineeringError('UNSUPPORTED_TARGET', 'Attribute-style descriptions require the legacy description tool');
|
|
280
|
+
const description = tag.children.find(child => child.kind === 'Description');
|
|
281
|
+
return description === undefined ? tag.selfClosing ? insert(xml, tag, `<Description>${escape(operation.description)}</Description>`) : replace(xml, tag.contentStart, tag.contentStart, `<Description>${escape(operation.description)}</Description>`)
|
|
282
|
+
: replace(xml, description.start, description.end, `<Description>${escape(operation.description)}</Description>`);
|
|
283
|
+
}
|
|
284
|
+
const owner = program(operation.program);
|
|
285
|
+
const routines = uniqueRegion(owner.children, 'Routines');
|
|
286
|
+
if (operation.operation === 'add_routine') {
|
|
287
|
+
if (routines.children.some(routine => routine.attributes.Name?.toLowerCase() === operation.name.toLowerCase()))
|
|
288
|
+
throw new EngineeringError('NAME_COLLISION', 'Routine already exists');
|
|
289
|
+
let body;
|
|
290
|
+
if (operation.language === 'ST')
|
|
291
|
+
body = stContent(operation.code);
|
|
292
|
+
else {
|
|
293
|
+
const lines = operation.code.split('\n').filter(line => line.trim() !== '');
|
|
294
|
+
if (lines.some(line => parseLadder(line).issues.length > 0))
|
|
295
|
+
throw new EngineeringError('INVALID_LADDER', 'Each nonempty line must contain one complete rung');
|
|
296
|
+
body = '<RLLContent>' + lines.map((line, index) => `<Rung Number="${index}" Type="N"><Text>${cdata(line)}</Text></Rung>`).join('') + '</RLLContent>';
|
|
297
|
+
}
|
|
298
|
+
return insert(xml, routines, `<Routine Name="${operation.name}" Type="${operation.language}">${body}</Routine>`);
|
|
299
|
+
}
|
|
300
|
+
const routine = uniqueRegion(routines.children, 'Routine', operation.routine);
|
|
301
|
+
editable(routine);
|
|
302
|
+
if (operation.operation === 'replace_st') {
|
|
303
|
+
if (routine.attributes.Type !== 'ST')
|
|
304
|
+
throw new EngineeringError('UNSUPPORTED_TARGET', 'Selected routine is not Structured Text');
|
|
305
|
+
const content = uniqueRegion(routine.children, 'STContent');
|
|
306
|
+
return replace(xml, content.start, content.end, stContent(operation.code));
|
|
307
|
+
}
|
|
308
|
+
if (routine.attributes.Type !== 'RLL')
|
|
309
|
+
throw new EngineeringError('UNSUPPORTED_TARGET', 'Selected routine is not ladder');
|
|
310
|
+
const content = uniqueRegion(routine.children, 'RLLContent');
|
|
311
|
+
const rungs = content.children.filter(child => child.kind === 'Rung');
|
|
312
|
+
const targets = rungs.filter(rung => Number(rung.attributes.Number) === operation.rung);
|
|
313
|
+
if (operation.operation === 'insert_rung') {
|
|
314
|
+
if (targets.length !== 0)
|
|
315
|
+
throw new EngineeringError('NAME_COLLISION', 'Rung number already exists; no automatic renumbering is performed');
|
|
316
|
+
const newRung = `<Rung Number="${operation.rung}" Type="N"><Text>${cdata(operation.text)}</Text></Rung>`;
|
|
317
|
+
if (operation.before_rung === undefined)
|
|
318
|
+
return insert(xml, content, newRung);
|
|
319
|
+
const before = rungs.filter(rung => Number(rung.attributes.Number) === operation.before_rung);
|
|
320
|
+
if (before.length !== 1)
|
|
321
|
+
throw new EngineeringError('AMBIGUOUS_TARGET', 'Insertion target rung is absent or ambiguous');
|
|
322
|
+
return replace(xml, before[0].start, before[0].start, newRung);
|
|
323
|
+
}
|
|
324
|
+
if (targets.length !== 1)
|
|
325
|
+
throw new EngineeringError('AMBIGUOUS_TARGET', 'Selected rung is absent or ambiguous');
|
|
326
|
+
const target = targets[0];
|
|
327
|
+
if (operation.operation === 'delete_rung')
|
|
328
|
+
return replace(xml, target.start, target.end, '');
|
|
329
|
+
const contentText = uniqueRegion(target.children, 'Text');
|
|
330
|
+
return replace(xml, contentText.start, contentText.end, `<Text>${cdata(operation.text)}</Text>`);
|
|
331
|
+
}
|
|
332
|
+
/** Every type name a change may reference: scalars plus declared types. */
|
|
333
|
+
function declaredTypeNames(project) {
|
|
334
|
+
const names = new Set([...SCALAR_TYPES].map(name => name.toLowerCase()));
|
|
335
|
+
for (const name of typeIndex(project.document).keys())
|
|
336
|
+
names.add(name);
|
|
337
|
+
return names;
|
|
338
|
+
}
|
|
339
|
+
/** Baseline audit for one snapshot, reused by every preparation against it. */
|
|
340
|
+
const baselineAudits = new WeakMap();
|
|
341
|
+
/** Last full preparation per snapshot, so apply does not recompute checks and diff. */
|
|
342
|
+
const preparedCache = new WeakMap();
|
|
343
|
+
function findingKey(finding) { return JSON.stringify([finding.rule, finding.severity, finding.message, finding.evidence]); }
|
|
344
|
+
function baselineAudit(project) {
|
|
345
|
+
const cached = baselineAudits.get(project);
|
|
346
|
+
if (cached !== undefined)
|
|
347
|
+
return cached;
|
|
348
|
+
const audit = runChecks(project);
|
|
349
|
+
baselineAudits.set(project, audit);
|
|
350
|
+
return audit;
|
|
351
|
+
}
|
|
352
|
+
/** Findings this draft adds or resolves, not the whole project audit. */
|
|
353
|
+
function deltaChecks(project, draft) {
|
|
354
|
+
const before = baselineAudit(project);
|
|
355
|
+
const after = runChecks(draft, project);
|
|
356
|
+
const beforeKeys = new Set(before.findings.map(findingKey));
|
|
357
|
+
const afterKeys = new Set(after.findings.map(findingKey));
|
|
358
|
+
const added = after.findings.filter(finding => !beforeKeys.has(findingKey(finding)));
|
|
359
|
+
const resolved = before.findings.filter(finding => !afterKeys.has(findingKey(finding)));
|
|
360
|
+
return {
|
|
361
|
+
summary: after.summary,
|
|
362
|
+
baseline_summary: before.summary,
|
|
363
|
+
new_finding_count: added.length,
|
|
364
|
+
resolved_count: resolved.length,
|
|
365
|
+
new_findings: added.slice(0, 25),
|
|
366
|
+
truncated: added.length > 25,
|
|
367
|
+
note: 'Lists only findings this draft adds or resolves; call run_checks for the full project audit.',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** Prepare every operation in memory, without writing a draft or touching Studio.
|
|
371
|
+
* @param project - Hash-verified baseline.
|
|
372
|
+
* @param request - Version-1 JSON change set.
|
|
373
|
+
* @param limits - Validated operation and draft-size limits.
|
|
374
|
+
* @returns Draft text and review data; malformed, unsupported or protected changes throw.
|
|
375
|
+
*/
|
|
376
|
+
export function prepareChangeSet(project, request, limits) {
|
|
377
|
+
if (Buffer.byteLength(request, 'utf8') > limits.bytes)
|
|
378
|
+
throw new EngineeringError('REQUEST_TOO_LARGE', 'Change-set request exceeds maxFileBytes');
|
|
379
|
+
if (!/<\?xml[^>]*encoding=["'](?:UTF-8|utf-8)["']/u.test(project.xml) && /<\?xml[^>]*encoding=/u.test(project.xml))
|
|
380
|
+
throw new EngineeringError('UNSUPPORTED_ENCODING', 'Change-set writing requires UTF-8 exports');
|
|
381
|
+
const cacheKey = `${limits.operations}:${limits.bytes}:${createHash('sha256').update(request).digest('hex')}`;
|
|
382
|
+
const cached = preparedCache.get(project);
|
|
383
|
+
if (cached?.key === cacheKey)
|
|
384
|
+
return { ...cached.result, prepared: 'cache' };
|
|
385
|
+
const operations = parseOperations(request, limits.operations, declaredTypeNames(project));
|
|
386
|
+
let xml = project.xml;
|
|
387
|
+
const encoded = (text) => [...text.matchAll(/<EncodedData\b[^>]*>[\s\S]*?<\/EncodedData>/gu)].map(match => match[0]);
|
|
388
|
+
const protectedBefore = encoded(project.xml);
|
|
389
|
+
for (const operation of operations) {
|
|
390
|
+
xml = applyOperation(xml, operation, { document: project.document });
|
|
391
|
+
if (Buffer.byteLength(xml, 'utf8') > limits.bytes)
|
|
392
|
+
throw new EngineeringError('DRAFT_TOO_LARGE', 'Draft exceeds maxFileBytes');
|
|
393
|
+
}
|
|
394
|
+
if (xml === project.xml)
|
|
395
|
+
throw new EngineeringError('NO_CHANGE', 'Change set does not alter the project');
|
|
396
|
+
if (JSON.stringify(encoded(xml)) !== JSON.stringify(protectedBefore))
|
|
397
|
+
throw new EngineeringError('PROTECTED_TARGET', 'Encoded definitions changed');
|
|
398
|
+
const document = loadL5x(xml);
|
|
399
|
+
const sha256 = createHash('sha256').update(xml).digest('hex');
|
|
400
|
+
const draft = { ...project, xml, document, sha256, bytes: Buffer.byteLength(xml, 'utf8') };
|
|
401
|
+
const result = { xml, operations, source_sha256: project.sha256, draft_sha256: sha256, diff: compareProjects(project, draft), checks: deltaChecks(project, draft), validation: { xml: 'passed', studio_import: 'not_run', studio_build: 'not_run', behavior: 'not_run' }, prepared: 'computed' };
|
|
402
|
+
preparedCache.set(project, { key: cacheKey, result });
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
/** One changed data type as a Studio Data Type import component.
|
|
406
|
+
* @param project - Hash-verified baseline.
|
|
407
|
+
* @param request - Version-1 JSON change set holding one or more UDT operations.
|
|
408
|
+
* @returns Component L5X text plus the type-level difference; no file is written.
|
|
409
|
+
*/
|
|
410
|
+
export function prepareUdtComponent(project, request) {
|
|
411
|
+
if (!/<\?xml[^>]*encoding=["'](?:UTF-8|utf-8)["']/u.test(project.xml) && /<\?xml[^>]*encoding=/u.test(project.xml))
|
|
412
|
+
throw new EngineeringError('UNSUPPORTED_ENCODING', 'Component writing requires UTF-8 exports');
|
|
413
|
+
const operations = parseOperations(request, 100, declaredTypeNames(project));
|
|
414
|
+
const udtOperations = operations.filter(isUdtOperation);
|
|
415
|
+
if (udtOperations.length !== operations.length || udtOperations.length === 0)
|
|
416
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'A UDT component accepts only add_udt_member, set_udt_description and set_udt_member_description operations');
|
|
417
|
+
const target = udtOperations[0].udt;
|
|
418
|
+
if (udtOperations.some(operation => operation.udt.toLowerCase() !== target.toLowerCase()))
|
|
419
|
+
throw new EngineeringError('INVALID_CHANGE_SET', 'A UDT component changes exactly one data type');
|
|
420
|
+
let xml = project.xml;
|
|
421
|
+
for (const operation of operations)
|
|
422
|
+
xml = applyOperation(xml, operation, { document: project.document, allowUdtInstances: true });
|
|
423
|
+
const root = uniqueRegion(xmlRegions(xml), 'RSLogix5000Content');
|
|
424
|
+
const controller = uniqueRegion(root.children, 'Controller');
|
|
425
|
+
const dataTypes = uniqueRegion(controller.children, 'DataTypes');
|
|
426
|
+
const type = uniqueRegion(dataTypes.children, 'DataType', target);
|
|
427
|
+
const before = udtDetail(project.document, target);
|
|
428
|
+
const after = udtDetail(loadL5x(xml), target);
|
|
429
|
+
if (before === undefined || after === undefined)
|
|
430
|
+
throw new EngineeringError('COMPONENT_NOT_FOUND', `Data type ${target} not found`);
|
|
431
|
+
const component = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<RSLogix5000Content SchemaRevision="1.0" SoftwareRevision="${project.softwareRevision}" TargetName="${escape(target)}" TargetType="DataType" ContainsContext="false"><Controller Use="Context" Name="${escape(project.document.controller.name)}"><DataTypes>${xml.slice(type.start, type.end)}</DataTypes></Controller></RSLogix5000Content>\n`;
|
|
432
|
+
return { xml: component, operations, udt: target, source_sha256: project.sha256,
|
|
433
|
+
component_sha256: createHash('sha256').update(component).digest('hex'),
|
|
434
|
+
changes: { before, after }, validation: { xml: 'passed', studio_import: 'not_run' } };
|
|
435
|
+
}
|
|
436
|
+
/** Stage a fully prepared change set into a new file, retaining proposal validation compatibility.
|
|
437
|
+
* @param ctx - Filesystem service.
|
|
438
|
+
* @param session - Calling chat's project/proposal records.
|
|
439
|
+
* @param project - Current source snapshot.
|
|
440
|
+
* @param request - Version-1 operations JSON.
|
|
441
|
+
* @param outputPath - New draft beside the source.
|
|
442
|
+
* @param expectedHash - Baseline hash reviewed during preparation.
|
|
443
|
+
* @param limits - Operation and byte limits.
|
|
444
|
+
* @param signal - Cancellation for filesystem operations.
|
|
445
|
+
* @returns Proposal identity, hash-bound changes and static validation; no SDK or controller operations.
|
|
446
|
+
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { L5xDocument, L5xTag } from '../model/types.ts';
|
|
2
|
+
export declare function aliasesOf(document: L5xDocument): ReadonlyMap<string, string>;
|
|
3
|
+
export declare function engineeringTags(document: L5xDocument): readonly (L5xTag & {
|
|
4
|
+
readonly aliasFor?: string;
|
|
5
|
+
})[];
|
|
6
|
+
/** Module attributes and every port, excluding runtime data values and export formatting. */
|
|
7
|
+
export declare function moduleConfigurations(document: L5xDocument): {
|
|
8
|
+
name: string;
|
|
9
|
+
catalogNumber: string;
|
|
10
|
+
parentModule: string;
|
|
11
|
+
parentPortId: string;
|
|
12
|
+
major: string;
|
|
13
|
+
minor: string;
|
|
14
|
+
inhibited: string;
|
|
15
|
+
ports: {
|
|
16
|
+
id: string;
|
|
17
|
+
address: string;
|
|
18
|
+
type: string;
|
|
19
|
+
upstream: string;
|
|
20
|
+
}[];
|
|
21
|
+
}[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { controllerElementOf } from "../model/parse.js";
|
|
2
|
+
import { elementChildren, firstChild } from "../model/xml.js";
|
|
3
|
+
function tagEntries(container, scope) {
|
|
4
|
+
const tags = container === undefined ? undefined : firstChild(container, 'Tags');
|
|
5
|
+
if (tags === undefined)
|
|
6
|
+
return [];
|
|
7
|
+
return elementChildren(tags, 'Tag').flatMap(tag => tag.attrs['AliasFor'] === undefined ? [] : [{ tag: tag.attrs['Name'] ?? '', scope, aliasFor: tag.attrs['AliasFor'] }]);
|
|
8
|
+
}
|
|
9
|
+
export function aliasesOf(document) {
|
|
10
|
+
const controller = controllerElementOf(document);
|
|
11
|
+
const rows = tagEntries(controller, 'controller');
|
|
12
|
+
const programs = firstChild(controller, 'Programs');
|
|
13
|
+
if (programs !== undefined)
|
|
14
|
+
for (const program of elementChildren(programs, 'Program'))
|
|
15
|
+
rows.push(...tagEntries(program, program.attrs['Name'] ?? ''));
|
|
16
|
+
return new Map(rows.map(row => [`${row.scope}::${row.tag}`, row.aliasFor]));
|
|
17
|
+
}
|
|
18
|
+
export function engineeringTags(document) {
|
|
19
|
+
const aliases = aliasesOf(document);
|
|
20
|
+
return document.tags.map(tag => {
|
|
21
|
+
const aliasFor = aliases.get(`${tag.scope}::${tag.name}`);
|
|
22
|
+
return aliasFor === undefined ? tag : { ...tag, aliasFor };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Module attributes and every port, excluding runtime data values and export formatting. */
|
|
26
|
+
export function moduleConfigurations(document) {
|
|
27
|
+
const controller = controllerElementOf(document);
|
|
28
|
+
const modules = firstChild(controller, 'Modules');
|
|
29
|
+
if (modules === undefined)
|
|
30
|
+
return [];
|
|
31
|
+
return elementChildren(modules, 'Module').map(module => {
|
|
32
|
+
const ports = firstChild(module, 'Ports');
|
|
33
|
+
return {
|
|
34
|
+
name: module.attrs['Name'] ?? '', catalogNumber: module.attrs['CatalogNumber'] ?? '',
|
|
35
|
+
parentModule: module.attrs['ParentModule'] ?? '', parentPortId: module.attrs['ParentModPortId'] ?? '',
|
|
36
|
+
major: module.attrs['Major'] ?? '', minor: module.attrs['Minor'] ?? '', inhibited: module.attrs['Inhibited'] ?? '',
|
|
37
|
+
ports: ports === undefined ? [] : elementChildren(ports, 'Port').map(port => ({
|
|
38
|
+
id: port.attrs['Id'] ?? '', address: port.attrs['Address'] ?? '', type: port.attrs['Type'] ?? '', upstream: port.attrs['Upstream'] ?? '',
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail readers that the engineering tools need and the legacy byte-compatible
|
|
3
|
+
* readers must not gain.
|
|
4
|
+
*
|
|
5
|
+
* The `l5x_*` tools mirror another server's JSON exactly, so a field added to
|
|
6
|
+
* `L5xTag` would break that contract. These readers answer from the same parsed
|
|
7
|
+
* document but are only used by the engineering tools: array dimensions, stored
|
|
8
|
+
* initial values, alias targets, BIT backing members and the full data-type
|
|
9
|
+
* table (which includes module-defined `AB:` types that `document.udts` omits).
|
|
10
|
+
*/
|
|
11
|
+
import type { L5xDocument, L5xTag, L5xUdt } from '../model/types.ts';
|
|
12
|
+
import { type XmlElement } from '../model/xml.ts';
|
|
13
|
+
/** One member as the export declares it, including BIT placement. */
|
|
14
|
+
export interface TypeMember {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly dataType: string;
|
|
17
|
+
readonly dimension: number;
|
|
18
|
+
readonly target?: string;
|
|
19
|
+
readonly bitNumber?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Tag fields the legacy reader does not report. */
|
|
22
|
+
export interface TagDetail {
|
|
23
|
+
readonly dimensions?: string;
|
|
24
|
+
readonly value?: string;
|
|
25
|
+
readonly aliasFor?: string;
|
|
26
|
+
readonly words?: number;
|
|
27
|
+
}
|
|
28
|
+
/** Every data type the export declares, including module-defined families. */
|
|
29
|
+
export declare function typeIndex(document: L5xDocument): ReadonlyMap<string, ReadonlyMap<string, TypeMember>>;
|
|
30
|
+
/** One declared type's member table, by case-insensitive name. */
|
|
31
|
+
export declare function membersOf(document: L5xDocument, typeName: string): ReadonlyMap<string, TypeMember> | undefined;
|
|
32
|
+
/** One member of a declared type. */
|
|
33
|
+
export declare function memberOf(document: L5xDocument, typeName: string, memberName: string): TypeMember | undefined;
|
|
34
|
+
/** The exact `<Tag>` element for a scope and name. */
|
|
35
|
+
export declare function tagElement(document: L5xDocument, scope: string, name: string): XmlElement | undefined;
|
|
36
|
+
/** Dimensions, stored initial value and alias target of one tag. */
|
|
37
|
+
export declare function tagDetail(document: L5xDocument, tag: L5xTag): TagDetail;
|
|
38
|
+
/** One UDT's visible members with BIT placement and array length. */
|
|
39
|
+
export declare function udtDetail(document: L5xDocument, name: string): L5xUdt | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Every tag, AOI parameter and AOI local tag whose type tree contains the type.
|
|
42
|
+
*
|
|
43
|
+
* A UDT change rewrites the serialized data of every instance, including
|
|
44
|
+
* instances of types that nest it, so the whole-project change set only accepts
|
|
45
|
+
* a member change when this list is empty.
|
|
46
|
+
*/
|
|
47
|
+
export declare function typeInstances(document: L5xDocument, typeName: string): readonly string[];
|