@aryacode/dsh-engineering 0.4.7 → 0.4.21

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 (162) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +40 -26
  3. package/cordis.patch.yml +7 -6
  4. package/entries/config.js +62 -0
  5. package/entries/ftview.js +5 -4
  6. package/entries/l5x.js +47 -15
  7. package/entries/mwi.js +26 -6
  8. package/entries/optix.js +105 -0
  9. package/lib/client.js +335 -6
  10. package/package.json +11 -10
  11. package/vendor/ftview/SKILL.md +17 -0
  12. package/vendor/l5x/SKILL.md +17 -0
  13. package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
  14. package/vendor/mwi/README.md +41 -18
  15. package/vendor/mwi/SKILL.md +39 -20
  16. package/vendor/mwi/bridge/mw_bridge.ps1 +2768 -2763
  17. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
  18. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
  19. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  21. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  22. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
  23. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
  24. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
  25. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
  26. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
  27. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
  28. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
  29. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
  30. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
  31. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +134 -130
  32. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
  33. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
  34. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
  35. package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
  36. package/vendor/mwi/lib/core/code-engine.js +18 -1
  37. package/vendor/mwi/lib/edit.d.ts +1 -1
  38. package/vendor/mwi/lib/edit.js +1 -1
  39. package/vendor/mwi/lib/read/service.js +8 -1
  40. package/vendor/mwi/lib/session/backup.d.ts +72 -0
  41. package/vendor/mwi/lib/session/backup.js +257 -0
  42. package/vendor/mwi/lib/session/project.d.ts +4 -1
  43. package/vendor/mwi/lib/session/project.js +13 -2
  44. package/vendor/mwi/lib/session/service.d.ts +13 -6
  45. package/vendor/mwi/lib/session/service.js +127 -36
  46. package/vendor/mwi/lib/session/workspace.d.ts +10 -0
  47. package/vendor/mwi/lib/session/workspace.js +53 -3
  48. package/vendor/mwi/lib/skill.d.ts +2 -2
  49. package/vendor/mwi/lib/skill.js +2 -2
  50. package/vendor/mwi/lib/tools/edit.d.ts +3 -3
  51. package/vendor/mwi/lib/tools/edit.js +14 -15
  52. package/vendor/mwi/lib/tools/read.js +1 -1
  53. package/vendor/mwi/lib/tools/session.js +13 -9
  54. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
  55. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
  56. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
  57. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
  58. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  59. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
  60. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
  61. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
  62. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
  63. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
  64. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  65. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  66. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
  67. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
  68. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  69. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
  70. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
  71. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
  72. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
  73. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
  74. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
  75. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  76. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
  77. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
  78. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  79. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
  80. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
  81. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
  82. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
  83. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
  84. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
  85. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  86. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
  87. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
  88. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
  89. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
  90. package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
  91. package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  92. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
  93. package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
  94. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
  95. package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
  96. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
  97. package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  98. package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  99. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
  100. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
  101. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  102. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
  103. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
  104. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
  105. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
  106. package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
  107. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
  108. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  109. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
  110. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
  111. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
  112. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
  113. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
  114. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
  115. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
  116. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
  117. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
  118. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  119. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
  120. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
  121. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
  122. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
  123. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
  124. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
  125. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
  126. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
  127. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
  128. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
  129. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
  130. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
  131. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
  132. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
  133. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
  134. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
  135. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
  136. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
  137. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
  138. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
  139. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
  140. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  141. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
  142. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
  143. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
  144. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  145. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  146. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  147. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  148. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  149. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  150. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  151. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  152. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  153. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  154. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  155. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  156. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  157. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  158. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  159. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  160. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  161. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  162. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @module dsh-engineering-mwi/core/code-engine
10
10
  */
11
+ import { MwiError } from './errors.ts';
11
12
  /** Where the engine lives and which workspace it serves. */
12
13
  export interface CodeEngineOptions {
13
14
  /** Absolute workspace root. */
@@ -23,6 +24,18 @@ export interface CodeEngineOptions {
23
24
  export type EngineResult = Readonly<Record<string, unknown>>;
24
25
  /** Resolve the first usable Python 3 interpreter. */
25
26
  export declare function findPython(): string;
27
+ /**
28
+ * Translate one engine refusal into the failure the caller should act on.
29
+ *
30
+ * The engine guard is shared with the retired staging flow, so its refusal says
31
+ * "stage a copy". In this bundle a real workspace project is opened directly, and
32
+ * the identity record that an open writes is exactly what the guard accepts: point
33
+ * at the open instead of letting the caller loop back into staging.
34
+ *
35
+ * @param message - The refusal text the engine wrote.
36
+ * @returns The error to throw.
37
+ */
38
+ export declare function engineFailure(message: string): MwiError;
26
39
  /** The typed engine client. */
27
40
  export declare class CodeEngine {
28
41
  private readonly options;
@@ -28,6 +28,23 @@ export function findPython() {
28
28
  }
29
29
  return 'python';
30
30
  }
31
+ /**
32
+ * Translate one engine refusal into the failure the caller should act on.
33
+ *
34
+ * The engine guard is shared with the retired staging flow, so its refusal says
35
+ * "stage a copy". In this bundle a real workspace project is opened directly, and
36
+ * the identity record that an open writes is exactly what the guard accepts: point
37
+ * at the open instead of letting the caller loop back into staging.
38
+ *
39
+ * @param message - The refusal text the engine wrote.
40
+ * @returns The error to throw.
41
+ */
42
+ export function engineFailure(message) {
43
+ if (/outside the staging root/iu.test(message)) {
44
+ return new MwiError('NOT_ATTACHED', message, 'Call mw_project_open with this project first: it writes the verified attachment record the engine guard requires. mw_project_stage is only an optional sandbox.');
45
+ }
46
+ return new MwiError('ENGINE_REFUSED', message, 'Read the message, correct the request, and retry. Do not retry the same invalid request.');
47
+ }
31
48
  /** The typed engine client. */
32
49
  export class CodeEngine {
33
50
  options;
@@ -83,7 +100,7 @@ export class CodeEngine {
83
100
  if (existsSync(responsePath)) {
84
101
  const payload = await this.readResponse(responsePath);
85
102
  if (payload.ok === false) {
86
- throw new MwiError('ENGINE_REFUSED', typeof payload.error === 'string' ? payload.error : `The MotionWorks engine refused ${verb}.`, 'Read the message, correct the request, and retry. Do not retry the same invalid request.');
103
+ throw engineFailure(typeof payload.error === 'string' ? payload.error : `The MotionWorks engine refused ${verb}.`);
87
104
  }
88
105
  return payload;
89
106
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `mwi-edit` entry: staged edits, validation and apply.
3
3
  *
4
- * Off by default. The row writes only the staged project and verifies that
4
+ * On by default; a read-only profile can switch it off. The row writes only the staged project and verifies that
5
5
  * unrelated saved and native state did not change.
6
6
  *
7
7
  * @module dsh-engineering-mwi/edit
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `mwi-edit` entry: staged edits, validation and apply.
3
3
  *
4
- * Off by default. The row writes only the staged project and verifies that
4
+ * On by default; a read-only profile can switch it off. The row writes only the staged project and verifies that
5
5
  * unrelated saved and native state did not change.
6
6
  *
7
7
  * @module dsh-engineering-mwi/edit
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module dsh-engineering-mwi/read/service
8
8
  */
9
+ import { createHash } from 'node:crypto';
9
10
  import { MwiError } from "../core/errors.js";
10
11
  /** Kinds `mw_list` can enumerate. */
11
12
  export const LIST_KINDS = ['pou', 'variable', 'global', 'type', 'task', 'library'];
@@ -52,7 +53,13 @@ export class ReadService {
52
53
  async get(project, kind, name, library) {
53
54
  if (kind === 'pou') {
54
55
  const text = await this.engine.run('read_text', { project: project.directory, pou: name }, 60_000);
55
- return { ...text, project: project.directory };
56
+ // The edit preconditions take the hash of exactly this body. Computing it
57
+ // here is what makes the documented `expected_body_sha256` obtainable from
58
+ // this read at all: the engine returns the body, not its digest, and a
59
+ // caller that has to find the hash elsewhere is a caller that guesses.
60
+ const body = typeof text.body === 'string' ? text.body : undefined;
61
+ const digest = body === undefined ? {} : { text_body_sha256: createHash('sha256').update(body).digest('hex') };
62
+ return { ...text, project: project.directory, ...digest };
56
63
  }
57
64
  if (kind === 'block') {
58
65
  return await this.engine.run('block_interface', {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pre-edit backups shared by the automation surfaces.
3
+ *
4
+ * Editing happens on the real workspace project. The first write takes one
5
+ * complete, hash-verified copy under
6
+ * `<workspace>/backups/motionworks/<project>/<UTC stamp>`, writes a manifest
7
+ * beside it and records the project identity in
8
+ * `<workspace>/.motionworks/attached/records`.
9
+ *
10
+ * `<workspace>/backups/<product>/…` is the one backup tree every engineering
11
+ * product writes under, so "back up this project" means the same place whichever
12
+ * product is in front of you. MotionWorks keeps its identity records private
13
+ * under `.motionworks`, and both the shipped bridge (`Get-DirectIdentity`) and
14
+ * the read-only engine (`assert_direct_attached`) verify a record before they
15
+ * open, read or write a real project. A copy outside that tree is not evidence:
16
+ * those guards refuse it, so the tools must write exactly this shape. Records
17
+ * written before 0.4.21 that point at `.motionworks/attached/backups` stay valid.
18
+ *
19
+ * @module dsh-engineering-mwi/session/backup
20
+ */
21
+ /** One file in a backup manifest. */
22
+ export interface BackupFile {
23
+ /** Path relative to the project's parent directory. */
24
+ readonly path: string;
25
+ /** SHA-256 of the copied bytes. */
26
+ readonly sha256: string;
27
+ }
28
+ /** One verified pre-edit backup. */
29
+ export interface BackupRecord {
30
+ /** Project name, the `.mwt` basename. */
31
+ readonly name: string;
32
+ /** Workspace that owns the backup. */
33
+ readonly workspace: string;
34
+ /** The real `.mwt` the backup was taken from. */
35
+ readonly mwt: string;
36
+ /** The real expanded directory. */
37
+ readonly directory: string;
38
+ /** Directory holding the copy. */
39
+ readonly backup: string;
40
+ /** Manifest path inside the backup directory. */
41
+ readonly manifest: string;
42
+ /** When the copy was written. */
43
+ readonly created_at: string;
44
+ }
45
+ /**
46
+ * The workspace backup root every engineering product writes under:
47
+ * `<workspace>/backups/<product>/<subject>/<UTC stamp>/`.
48
+ *
49
+ * MotionWorks is the `motionworks` product. Keeping the copies in one visible
50
+ * tree is what makes "back up this project" mean the same place for every
51
+ * product; the identity records stay product-private under `.motionworks`.
52
+ */
53
+ export declare function backupRootOf(workspace: string): string;
54
+ /**
55
+ * The pre-0.4.21 backup root. Records written by earlier builds point here and
56
+ * stay valid: a shipped record is evidence, so it is accepted, never rewritten.
57
+ */
58
+ export declare function legacyBackupRootOf(workspace: string): string;
59
+ /** The identity record directory of one workspace. */
60
+ export declare function recordRootOf(workspace: string): string;
61
+ /** Read the record for a project, when a valid backup exists. */
62
+ export declare function findBackup(workspace: string, path: string): BackupRecord | undefined;
63
+ /** Every project with a valid backup in this workspace, one row per project. */
64
+ export declare function listBackups(workspace: string): BackupRecord[];
65
+ /**
66
+ * Back up one real project, or return the valid backup already recorded.
67
+ *
68
+ * @param workspace - Absolute workspace root.
69
+ * @param path - Real `.mwt` or its expanded directory.
70
+ * @returns The verified backup record.
71
+ */
72
+ export declare function ensureBackup(workspace: string, path: string): BackupRecord;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Pre-edit backups shared by the automation surfaces.
3
+ *
4
+ * Editing happens on the real workspace project. The first write takes one
5
+ * complete, hash-verified copy under
6
+ * `<workspace>/backups/motionworks/<project>/<UTC stamp>`, writes a manifest
7
+ * beside it and records the project identity in
8
+ * `<workspace>/.motionworks/attached/records`.
9
+ *
10
+ * `<workspace>/backups/<product>/…` is the one backup tree every engineering
11
+ * product writes under, so "back up this project" means the same place whichever
12
+ * product is in front of you. MotionWorks keeps its identity records private
13
+ * under `.motionworks`, and both the shipped bridge (`Get-DirectIdentity`) and
14
+ * the read-only engine (`assert_direct_attached`) verify a record before they
15
+ * open, read or write a real project. A copy outside that tree is not evidence:
16
+ * those guards refuse it, so the tools must write exactly this shape. Records
17
+ * written before 0.4.21 that point at `.motionworks/attached/backups` stay valid.
18
+ *
19
+ * @module dsh-engineering-mwi/session/backup
20
+ */
21
+ import { createHash, randomUUID } from 'node:crypto';
22
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
23
+ import { basename, dirname, join, relative, resolve } from 'node:path';
24
+ import { MwiError } from "../core/errors.js";
25
+ import { isInside } from "./workspace.js";
26
+ /**
27
+ * The workspace backup root every engineering product writes under:
28
+ * `<workspace>/backups/<product>/<subject>/<UTC stamp>/`.
29
+ *
30
+ * MotionWorks is the `motionworks` product. Keeping the copies in one visible
31
+ * tree is what makes "back up this project" mean the same place for every
32
+ * product; the identity records stay product-private under `.motionworks`.
33
+ */
34
+ export function backupRootOf(workspace) {
35
+ return join(workspace, 'backups', 'motionworks');
36
+ }
37
+ /**
38
+ * The pre-0.4.21 backup root. Records written by earlier builds point here and
39
+ * stay valid: a shipped record is evidence, so it is accepted, never rewritten.
40
+ */
41
+ export function legacyBackupRootOf(workspace) {
42
+ return join(workspace, '.motionworks', 'attached', 'backups');
43
+ }
44
+ /** The identity record directory of one workspace. */
45
+ export function recordRootOf(workspace) {
46
+ return join(workspace, '.motionworks', 'attached', 'records');
47
+ }
48
+ function digest(path) {
49
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
50
+ }
51
+ function same(left, right) {
52
+ return resolve(left).toLowerCase() === resolve(right).toLowerCase();
53
+ }
54
+ /** Refuse a link anywhere in a tree the backup will copy. */
55
+ function assertPlainTree(path, workspace) {
56
+ if (!isInside(workspace, path)) {
57
+ throw new MwiError('OUTSIDE_WORKSPACE', `${path} is outside ${workspace}.`, 'Back up a project from the calling workspace.');
58
+ }
59
+ if (lstatSync(path).isSymbolicLink()) {
60
+ throw new MwiError('LINKED_PROJECT', `The project contains a link: ${path}.`, 'Back up a project whose files are all local; links are refused.');
61
+ }
62
+ if (!statSync(path).isDirectory())
63
+ return;
64
+ for (const entry of readdirSync(path))
65
+ assertPlainTree(join(path, entry), workspace);
66
+ }
67
+ /** Every file under a directory, in stable order. */
68
+ function tree(path, files) {
69
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
70
+ const child = join(path, entry.name);
71
+ if (entry.isDirectory())
72
+ tree(child, files);
73
+ else
74
+ files.push(child);
75
+ }
76
+ }
77
+ /** Read and fully validate one identity record. */
78
+ function valid(workspace, record) {
79
+ try {
80
+ if (record.mode !== 'direct' || typeof record.mwt !== 'string' || typeof record.directory !== 'string')
81
+ return false;
82
+ if (!same(record.workspace, workspace))
83
+ return false;
84
+ if (!isInside(workspace, record.mwt) || !isInside(workspace, record.directory))
85
+ return false;
86
+ if (!same(record.directory, record.mwt.slice(0, -4)))
87
+ return false;
88
+ const root = backupRootOf(workspace);
89
+ const legacy = legacyBackupRootOf(workspace);
90
+ if (!isInside(root, record.backup) && !isInside(legacy, record.backup))
91
+ return false;
92
+ if (!isInside(record.backup, record.backup_manifest))
93
+ return false;
94
+ if (!existsSync(record.mwt) || !existsSync(record.directory) || !existsSync(record.backup_manifest))
95
+ return false;
96
+ if (digest(record.backup_manifest) !== record.backup_manifest_sha256)
97
+ return false;
98
+ const manifest = JSON.parse(readFileSync(record.backup_manifest, 'utf8'));
99
+ if (!Array.isArray(manifest.files))
100
+ return false;
101
+ for (const file of manifest.files) {
102
+ if (typeof file?.path !== 'string')
103
+ return false;
104
+ const member = join(record.backup, file.path);
105
+ if (!isInside(record.backup, member) || !existsSync(member))
106
+ return false;
107
+ }
108
+ assertPlainTree(record.directory, workspace);
109
+ return true;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ /** The record shape the tools return, from an on-disk identity record. */
116
+ function toBackup(record) {
117
+ return {
118
+ name: basename(record.mwt, '.mwt'),
119
+ workspace: record.workspace,
120
+ mwt: record.mwt,
121
+ directory: record.directory,
122
+ backup: record.backup,
123
+ manifest: record.backup_manifest,
124
+ created_at: record.attached_at,
125
+ };
126
+ }
127
+ /** Read every identity record in the workspace, valid or not. */
128
+ function records(workspace) {
129
+ let names = [];
130
+ try {
131
+ names = readdirSync(recordRootOf(workspace)).filter(name => name.endsWith('.json'));
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ const found = [];
137
+ for (const name of names) {
138
+ try {
139
+ const record = JSON.parse(readFileSync(join(recordRootOf(workspace), name), 'utf8'));
140
+ if (typeof record?.mwt === 'string' && typeof record?.directory === 'string')
141
+ found.push(record);
142
+ }
143
+ catch {
144
+ continue;
145
+ }
146
+ }
147
+ return found;
148
+ }
149
+ /** Read the record for a project, when a valid backup exists. */
150
+ export function findBackup(workspace, path) {
151
+ for (const record of records(workspace)) {
152
+ if (!same(path, record.mwt) && !same(path, record.directory))
153
+ continue;
154
+ if (!valid(workspace, record))
155
+ continue;
156
+ return toBackup(record);
157
+ }
158
+ return undefined;
159
+ }
160
+ /** Every project with a valid backup in this workspace, one row per project. */
161
+ export function listBackups(workspace) {
162
+ const found = [];
163
+ for (const record of records(workspace)) {
164
+ if (!valid(workspace, record))
165
+ continue;
166
+ if (found.some(existing => same(existing.mwt, record.mwt)))
167
+ continue;
168
+ found.push(toBackup(record));
169
+ }
170
+ return found;
171
+ }
172
+ /**
173
+ * Back up one real project, or return the valid backup already recorded.
174
+ *
175
+ * @param workspace - Absolute workspace root.
176
+ * @param path - Real `.mwt` or its expanded directory.
177
+ * @returns The verified backup record.
178
+ */
179
+ export function ensureBackup(workspace, path) {
180
+ const asked = resolve(path);
181
+ const mwt = asked.toLowerCase().endsWith('.mwt') ? asked : `${asked}.mwt`;
182
+ const directory = mwt.slice(0, -4);
183
+ if (!existsSync(mwt) || !existsSync(directory)) {
184
+ throw new MwiError('PROJECT_INCOMPLETE', `${mwt} has no expanded project directory beside it.`, 'Open a complete project: the .mwt and its sibling folder.');
185
+ }
186
+ if (!isInside(workspace, mwt) || !isInside(workspace, directory)) {
187
+ throw new MwiError('OUTSIDE_WORKSPACE', `${mwt} is outside ${workspace}.`, 'Work on a project inside the calling workspace.');
188
+ }
189
+ if (isInside(join(workspace, '.motionworks'), mwt)) {
190
+ throw new MwiError('REFUSED', `${mwt} lives under .motionworks.`, 'Use the staged-project path for a project in the stage.');
191
+ }
192
+ assertPlainTree(mwt, workspace);
193
+ assertPlainTree(directory, workspace);
194
+ const existing = findBackup(workspace, mwt);
195
+ if (existing !== undefined)
196
+ return existing;
197
+ // A record that no longer validates must be inspected, not silently replaced:
198
+ // a missing or altered backup member is the evidence a restore would need.
199
+ for (const record of records(workspace)) {
200
+ if (same(record.mwt, mwt)) {
201
+ throw new MwiError('BACKUP_INVALID', `The existing backup identity for ${mwt} does not verify.`, `Inspect ${record.backup} and its manifest before attaching again; a tampered or incomplete backup is not replaced automatically.`);
202
+ }
203
+ }
204
+ const id = randomUUID();
205
+ const name = basename(mwt, '.mwt');
206
+ const stamp = new Date().toISOString().replace(/[:.]/gu, '-');
207
+ const target = join(backupRootOf(workspace), name, stamp);
208
+ const files = [];
209
+ const sources = [mwt];
210
+ tree(directory, sources);
211
+ const before = sources.map(source => ({ source, sha256: digest(source) }));
212
+ mkdirSync(target, { recursive: true });
213
+ try {
214
+ for (const item of before) {
215
+ const rel = relative(dirname(mwt), item.source);
216
+ const destination = join(target, rel);
217
+ mkdirSync(dirname(destination), { recursive: true });
218
+ copyFileSync(item.source, destination);
219
+ if (digest(destination) !== item.sha256 || digest(item.source) !== item.sha256) {
220
+ throw new MwiError('BACKUP_UNSTABLE', `The project changed while it was being copied: ${item.source}.`, `Keep ${target} for inspection and retry on an unchanged project.`);
221
+ }
222
+ files.push({ path: rel, sha256: item.sha256 });
223
+ }
224
+ for (const item of before) {
225
+ if (digest(item.source) !== item.sha256) {
226
+ throw new MwiError('BACKUP_UNSTABLE', `The project changed after it was copied: ${item.source}.`, `Keep ${target} for inspection and retry on an unchanged project.`);
227
+ }
228
+ }
229
+ }
230
+ catch (error) {
231
+ if (error instanceof MwiError)
232
+ throw error;
233
+ throw new MwiError('BACKUP_FAILED', error.message, `Keep ${target} for inspection; no identity was written.`);
234
+ }
235
+ const manifest = join(target, 'backup-manifest.json');
236
+ writeFileSync(manifest, JSON.stringify({ workspace, mwt, directory, files }, null, 2));
237
+ const record = {
238
+ mode: 'direct',
239
+ workspace,
240
+ mwt,
241
+ directory,
242
+ session_id: null,
243
+ backup: target,
244
+ backup_manifest: manifest,
245
+ backup_manifest_sha256: digest(manifest),
246
+ attached_at: new Date().toISOString(),
247
+ };
248
+ mkdirSync(recordRootOf(workspace), { recursive: true });
249
+ const pending = join(recordRootOf(workspace), `${id}.json.tmp`);
250
+ const final = join(recordRootOf(workspace), `${id}.json`);
251
+ writeFileSync(pending, JSON.stringify(record, null, 2));
252
+ renameSync(pending, final);
253
+ if (!valid(workspace, record)) {
254
+ throw new MwiError('BACKUP_UNVERIFIED', `The backup at ${target} did not verify.`, `Keep ${target} for inspection and take a fresh copy of an unchanged project.`);
255
+ }
256
+ return toBackup(record);
257
+ }
@@ -28,7 +28,10 @@ export declare function resolveProject(workspace: string, requested: string): Pr
28
28
  /**
29
29
  * Resolve the project a call means when it names none.
30
30
  *
31
+ * An attached direct project wins over a stale staged copy: the direct
32
+ * workflow has no stage, and a leftover stage must not make the call ambiguous.
33
+ *
31
34
  * @param workspace - Absolute workspace root.
32
- * @returns The single staged project.
35
+ * @returns The single attached or staged project.
33
36
  */
34
37
  export declare function defaultProject(workspace: string): ProjectRef;
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readdirSync } from 'node:fs';
10
10
  import { basename, join, resolve } from 'node:path';
11
11
  import { MwiError } from "../core/errors.js";
12
+ import { listBackups } from "./backup.js";
12
13
  import { isInside, stageRootOf } from "./workspace.js";
13
14
  /**
14
15
  * Resolve one project path.
@@ -33,10 +34,20 @@ export function resolveProject(workspace, requested) {
33
34
  /**
34
35
  * Resolve the project a call means when it names none.
35
36
  *
37
+ * An attached direct project wins over a stale staged copy: the direct
38
+ * workflow has no stage, and a leftover stage must not make the call ambiguous.
39
+ *
36
40
  * @param workspace - Absolute workspace root.
37
- * @returns The single staged project.
41
+ * @returns The single attached or staged project.
38
42
  */
39
43
  export function defaultProject(workspace) {
44
+ const attached = listBackups(workspace);
45
+ if (attached.length === 1) {
46
+ return resolveProject(workspace, attached[0].mwt);
47
+ }
48
+ if (attached.length > 1) {
49
+ throw new MwiError('AMBIGUOUS_PROJECT', `${attached.length} projects have a pre-edit backup.`, 'Pass project: the project directory or .mwt.', attached.map(record => record.name));
50
+ }
40
51
  let names = [];
41
52
  try {
42
53
  names = readdirSync(stageRootOf(workspace)).filter(name => name.toLowerCase().endsWith('.mwt'));
@@ -45,7 +56,7 @@ export function defaultProject(workspace) {
45
56
  names = [];
46
57
  }
47
58
  if (names.length === 0) {
48
- throw new MwiError('NO_PROJECT', 'No project is staged in this workspace.', 'Call mw_project_find, then mw_project_stage, or pass project explicitly.');
59
+ throw new MwiError('NO_PROJECT', 'No project is open, backed up or staged in this workspace.', 'Call mw_project_find, then mw_project_open with the workspace .mwt.');
49
60
  }
50
61
  if (names.length > 1) {
51
62
  throw new MwiError('AMBIGUOUS_PROJECT', `${names.length} projects are staged.`, 'Pass project: the project directory or .mwt.', names.map(name => name.replace(/\.mwt$/iu, '')));
@@ -8,6 +8,7 @@
8
8
  * @module dsh-engineering-mwi/session/service
9
9
  */
10
10
  import { type StageOptions, type StageRecord } from './stage.ts';
11
+ import { type BackupRecord } from './backup.ts';
11
12
  import { ReadService } from '../read/service.ts';
12
13
  /** The bridge directory shipped with this package. */
13
14
  export declare const BRIDGE_DIR: string;
@@ -32,7 +33,9 @@ export interface SessionRuntime {
32
33
  ideState(): Promise<Record<string, unknown>>;
33
34
  /** Stage one workspace project. */
34
35
  stage(source: string, options?: StageOptions): Promise<StageRecord>;
35
- /** Open a staged project. */
36
+ /** Back up a real workspace project before the first write, or reuse its verified backup. */
37
+ attach(path: string): BackupRecord;
38
+ /** Open a staged or a real (backed-up) project. */
36
39
  open(path: string, options: {
37
40
  userApproved?: boolean;
38
41
  expectedProject?: string;
@@ -100,8 +103,12 @@ export declare class PowerShellSessionRuntime implements SessionRuntime {
100
103
  status(): Promise<Record<string, unknown>>;
101
104
  /** @returns The bridge's live state document. */
102
105
  ideState(): Promise<Record<string, unknown>>;
106
+ /** @returns The open project path, or undefined when no IDE or no project is open. */
107
+ private activeProject;
103
108
  /** @returns The written stage record. */
104
109
  stage(source: string, options?: StageOptions): Promise<StageRecord>;
110
+ /** @returns The verified pre-edit backup record. */
111
+ attach(path: string): BackupRecord;
105
112
  /** @returns The bridge's open result. */
106
113
  open(path: string, options: {
107
114
  userApproved?: boolean;
@@ -116,7 +123,7 @@ export declare class PowerShellSessionRuntime implements SessionRuntime {
116
123
  userApproved: boolean;
117
124
  expectedProject?: string;
118
125
  }): Promise<Record<string, unknown>>;
119
- /** @returns The stage record for a path. */
126
+ /** @returns The stage record for a path, when it is staged or directly backed up. */
120
127
  stagedAt(path: string): StageRecord | undefined;
121
128
  /** @returns The bridge's compile result. */
122
129
  compile(mode: 'make' | 'build' | 'state'): Promise<Record<string, unknown>>;
@@ -147,12 +154,12 @@ export declare class PowerShellSessionRuntime implements SessionRuntime {
147
154
  expectedBody?: string;
148
155
  expectedHash?: string;
149
156
  }): Promise<Record<string, unknown>>;
150
- /** The open project must be a proven stage; every compile and verify depends on it. */
157
+ /** The open project must be a proven stage or a verified direct backup; every compile and verify depends on it. */
151
158
  private openStagedRecord;
152
- /** The only staged project, or a refusal that explains the choice. */
159
+ /** The only openable project, staged or backed up, or a refusal that explains the choice. */
153
160
  private singleStage;
154
- /** Resolve a requested staged project. */
155
- private stageMwt;
161
+ /** Resolve a requested project, taking a backup for a real workspace project. */
162
+ private projectMwt;
156
163
  }
157
164
  /**
158
165
  * Resolve the runtime for one workspace.