@aryacode/dsh-engineering 0.4.6 → 0.4.20

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 (161) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +37 -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 +61 -13
  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/l5x/README.md +27 -5
  12. package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
  13. package/vendor/mwi/README.md +39 -18
  14. package/vendor/mwi/SKILL.md +26 -20
  15. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -2763
  16. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
  17. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
  18. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
  19. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
  20. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
  21. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
  22. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
  23. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
  24. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
  25. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
  26. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
  27. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +128 -128
  28. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
  29. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
  30. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
  31. package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
  32. package/vendor/mwi/lib/core/code-engine.js +18 -1
  33. package/vendor/mwi/lib/edit.d.ts +1 -1
  34. package/vendor/mwi/lib/edit.js +1 -1
  35. package/vendor/mwi/lib/read/service.js +8 -1
  36. package/vendor/mwi/lib/session/backup.d.ts +55 -0
  37. package/vendor/mwi/lib/session/backup.js +232 -0
  38. package/vendor/mwi/lib/session/project.d.ts +4 -1
  39. package/vendor/mwi/lib/session/project.js +13 -2
  40. package/vendor/mwi/lib/session/service.d.ts +13 -6
  41. package/vendor/mwi/lib/session/service.js +127 -36
  42. package/vendor/mwi/lib/session/workspace.d.ts +10 -0
  43. package/vendor/mwi/lib/session/workspace.js +53 -3
  44. package/vendor/mwi/lib/skill.d.ts +2 -2
  45. package/vendor/mwi/lib/skill.js +2 -2
  46. package/vendor/mwi/lib/tools/edit.d.ts +3 -3
  47. package/vendor/mwi/lib/tools/edit.js +14 -15
  48. package/vendor/mwi/lib/tools/read.js +1 -1
  49. package/vendor/mwi/lib/tools/session.js +13 -9
  50. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
  51. package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
  52. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
  53. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
  54. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  55. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
  56. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
  57. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
  58. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
  59. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
  60. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  61. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  62. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
  63. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
  64. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  65. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
  66. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
  67. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
  68. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
  69. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
  70. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
  71. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  72. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
  73. package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
  74. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  75. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
  76. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
  77. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
  78. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
  79. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
  80. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
  81. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  82. package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
  83. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
  84. package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
  85. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
  86. package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
  87. package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
  88. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
  89. package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
  90. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
  91. package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
  92. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
  93. package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
  94. package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
  95. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
  96. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
  97. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
  98. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
  99. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
  100. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
  101. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
  102. package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
  103. package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
  104. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
  105. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
  106. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
  107. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
  108. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
  109. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
  110. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
  111. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
  112. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
  113. package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
  114. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
  115. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
  116. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
  117. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
  118. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
  119. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
  120. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
  121. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
  122. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
  123. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
  124. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
  125. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
  126. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
  127. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
  128. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
  129. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
  130. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
  131. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
  132. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
  133. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
  134. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
  135. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
  136. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
  137. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
  138. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
  139. package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
  140. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  141. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  142. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  143. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  144. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  145. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  146. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  147. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  148. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  149. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  150. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  151. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  152. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  153. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  154. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  155. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  156. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  157. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  158. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  159. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  160. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  161. 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,55 @@
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>/.motionworks/attached/backups/<id>`, writes a manifest beside it
7
+ * and records the project identity in `<workspace>/.motionworks/attached/records`.
8
+ *
9
+ * That layout is the contract the shipped bridge (`Get-DirectIdentity`) and the
10
+ * read-only engine (`assert_direct_attached`) already verify before they open,
11
+ * read or write a real project. A copy anywhere else is not evidence: those
12
+ * guards refuse the open, so the tools must write exactly this shape.
13
+ *
14
+ * @module dsh-engineering-mwi/session/backup
15
+ */
16
+ /** One file in a backup manifest. */
17
+ export interface BackupFile {
18
+ /** Path relative to the project's parent directory. */
19
+ readonly path: string;
20
+ /** SHA-256 of the copied bytes. */
21
+ readonly sha256: string;
22
+ }
23
+ /** One verified pre-edit backup. */
24
+ export interface BackupRecord {
25
+ /** Project name, the `.mwt` basename. */
26
+ readonly name: string;
27
+ /** Workspace that owns the backup. */
28
+ readonly workspace: string;
29
+ /** The real `.mwt` the backup was taken from. */
30
+ readonly mwt: string;
31
+ /** The real expanded directory. */
32
+ readonly directory: string;
33
+ /** Directory holding the copy. */
34
+ readonly backup: string;
35
+ /** Manifest path inside the backup directory. */
36
+ readonly manifest: string;
37
+ /** When the copy was written. */
38
+ readonly created_at: string;
39
+ }
40
+ /** The shared backup root of one workspace. */
41
+ export declare function backupRootOf(workspace: string): string;
42
+ /** The identity record directory of one workspace. */
43
+ export declare function recordRootOf(workspace: string): string;
44
+ /** Read the record for a project, when a valid backup exists. */
45
+ export declare function findBackup(workspace: string, path: string): BackupRecord | undefined;
46
+ /** Every project with a valid backup in this workspace, one row per project. */
47
+ export declare function listBackups(workspace: string): BackupRecord[];
48
+ /**
49
+ * Back up one real project, or return the valid backup already recorded.
50
+ *
51
+ * @param workspace - Absolute workspace root.
52
+ * @param path - Real `.mwt` or its expanded directory.
53
+ * @returns The verified backup record.
54
+ */
55
+ export declare function ensureBackup(workspace: string, path: string): BackupRecord;
@@ -0,0 +1,232 @@
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>/.motionworks/attached/backups/<id>`, writes a manifest beside it
7
+ * and records the project identity in `<workspace>/.motionworks/attached/records`.
8
+ *
9
+ * That layout is the contract the shipped bridge (`Get-DirectIdentity`) and the
10
+ * read-only engine (`assert_direct_attached`) already verify before they open,
11
+ * read or write a real project. A copy anywhere else is not evidence: those
12
+ * guards refuse the open, so the tools must write exactly this shape.
13
+ *
14
+ * @module dsh-engineering-mwi/session/backup
15
+ */
16
+ import { createHash, randomUUID } from 'node:crypto';
17
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
18
+ import { basename, dirname, join, relative, resolve } from 'node:path';
19
+ import { MwiError } from "../core/errors.js";
20
+ import { isInside } from "./workspace.js";
21
+ /** The shared backup root of one workspace. */
22
+ export function backupRootOf(workspace) {
23
+ return join(workspace, '.motionworks', 'attached', 'backups');
24
+ }
25
+ /** The identity record directory of one workspace. */
26
+ export function recordRootOf(workspace) {
27
+ return join(workspace, '.motionworks', 'attached', 'records');
28
+ }
29
+ function digest(path) {
30
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
31
+ }
32
+ function same(left, right) {
33
+ return resolve(left).toLowerCase() === resolve(right).toLowerCase();
34
+ }
35
+ /** Refuse a link anywhere in a tree the backup will copy. */
36
+ function assertPlainTree(path, workspace) {
37
+ if (!isInside(workspace, path)) {
38
+ throw new MwiError('OUTSIDE_WORKSPACE', `${path} is outside ${workspace}.`, 'Back up a project from the calling workspace.');
39
+ }
40
+ if (lstatSync(path).isSymbolicLink()) {
41
+ throw new MwiError('LINKED_PROJECT', `The project contains a link: ${path}.`, 'Back up a project whose files are all local; links are refused.');
42
+ }
43
+ if (!statSync(path).isDirectory())
44
+ return;
45
+ for (const entry of readdirSync(path))
46
+ assertPlainTree(join(path, entry), workspace);
47
+ }
48
+ /** Every file under a directory, in stable order. */
49
+ function tree(path, files) {
50
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
51
+ const child = join(path, entry.name);
52
+ if (entry.isDirectory())
53
+ tree(child, files);
54
+ else
55
+ files.push(child);
56
+ }
57
+ }
58
+ /** Read and fully validate one identity record. */
59
+ function valid(workspace, record) {
60
+ try {
61
+ if (record.mode !== 'direct' || typeof record.mwt !== 'string' || typeof record.directory !== 'string')
62
+ return false;
63
+ if (!same(record.workspace, workspace))
64
+ return false;
65
+ if (!isInside(workspace, record.mwt) || !isInside(workspace, record.directory))
66
+ return false;
67
+ if (!same(record.directory, record.mwt.slice(0, -4)))
68
+ return false;
69
+ if (!isInside(backupRootOf(workspace), record.backup) || !isInside(record.backup, record.backup_manifest))
70
+ return false;
71
+ if (!existsSync(record.mwt) || !existsSync(record.directory) || !existsSync(record.backup_manifest))
72
+ return false;
73
+ if (digest(record.backup_manifest) !== record.backup_manifest_sha256)
74
+ return false;
75
+ const manifest = JSON.parse(readFileSync(record.backup_manifest, 'utf8'));
76
+ if (!Array.isArray(manifest.files))
77
+ return false;
78
+ for (const file of manifest.files) {
79
+ if (typeof file?.path !== 'string')
80
+ return false;
81
+ const member = join(record.backup, file.path);
82
+ if (!isInside(record.backup, member) || !existsSync(member))
83
+ return false;
84
+ }
85
+ assertPlainTree(record.directory, workspace);
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ /** The record shape the tools return, from an on-disk identity record. */
93
+ function toBackup(record) {
94
+ return {
95
+ name: basename(record.mwt, '.mwt'),
96
+ workspace: record.workspace,
97
+ mwt: record.mwt,
98
+ directory: record.directory,
99
+ backup: record.backup,
100
+ manifest: record.backup_manifest,
101
+ created_at: record.attached_at,
102
+ };
103
+ }
104
+ /** Read every identity record in the workspace, valid or not. */
105
+ function records(workspace) {
106
+ let names = [];
107
+ try {
108
+ names = readdirSync(recordRootOf(workspace)).filter(name => name.endsWith('.json'));
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ const found = [];
114
+ for (const name of names) {
115
+ try {
116
+ const record = JSON.parse(readFileSync(join(recordRootOf(workspace), name), 'utf8'));
117
+ if (typeof record?.mwt === 'string' && typeof record?.directory === 'string')
118
+ found.push(record);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ }
124
+ return found;
125
+ }
126
+ /** Read the record for a project, when a valid backup exists. */
127
+ export function findBackup(workspace, path) {
128
+ for (const record of records(workspace)) {
129
+ if (!same(path, record.mwt) && !same(path, record.directory))
130
+ continue;
131
+ if (!valid(workspace, record))
132
+ continue;
133
+ return toBackup(record);
134
+ }
135
+ return undefined;
136
+ }
137
+ /** Every project with a valid backup in this workspace, one row per project. */
138
+ export function listBackups(workspace) {
139
+ const found = [];
140
+ for (const record of records(workspace)) {
141
+ if (!valid(workspace, record))
142
+ continue;
143
+ if (found.some(existing => same(existing.mwt, record.mwt)))
144
+ continue;
145
+ found.push(toBackup(record));
146
+ }
147
+ return found;
148
+ }
149
+ /**
150
+ * Back up one real project, or return the valid backup already recorded.
151
+ *
152
+ * @param workspace - Absolute workspace root.
153
+ * @param path - Real `.mwt` or its expanded directory.
154
+ * @returns The verified backup record.
155
+ */
156
+ export function ensureBackup(workspace, path) {
157
+ const asked = resolve(path);
158
+ const mwt = asked.toLowerCase().endsWith('.mwt') ? asked : `${asked}.mwt`;
159
+ const directory = mwt.slice(0, -4);
160
+ if (!existsSync(mwt) || !existsSync(directory)) {
161
+ throw new MwiError('PROJECT_INCOMPLETE', `${mwt} has no expanded project directory beside it.`, 'Open a complete project: the .mwt and its sibling folder.');
162
+ }
163
+ if (!isInside(workspace, mwt) || !isInside(workspace, directory)) {
164
+ throw new MwiError('OUTSIDE_WORKSPACE', `${mwt} is outside ${workspace}.`, 'Work on a project inside the calling workspace.');
165
+ }
166
+ if (isInside(join(workspace, '.motionworks'), mwt)) {
167
+ throw new MwiError('REFUSED', `${mwt} lives under .motionworks.`, 'Use the staged-project path for a project in the stage.');
168
+ }
169
+ assertPlainTree(mwt, workspace);
170
+ assertPlainTree(directory, workspace);
171
+ const existing = findBackup(workspace, mwt);
172
+ if (existing !== undefined)
173
+ return existing;
174
+ // A record that no longer validates must be inspected, not silently replaced:
175
+ // a missing or altered backup member is the evidence a restore would need.
176
+ for (const record of records(workspace)) {
177
+ if (same(record.mwt, mwt)) {
178
+ 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.`);
179
+ }
180
+ }
181
+ const id = randomUUID();
182
+ const target = join(backupRootOf(workspace), id);
183
+ const files = [];
184
+ const sources = [mwt];
185
+ tree(directory, sources);
186
+ const before = sources.map(source => ({ source, sha256: digest(source) }));
187
+ mkdirSync(target, { recursive: true });
188
+ try {
189
+ for (const item of before) {
190
+ const rel = relative(dirname(mwt), item.source);
191
+ const destination = join(target, rel);
192
+ mkdirSync(dirname(destination), { recursive: true });
193
+ copyFileSync(item.source, destination);
194
+ if (digest(destination) !== item.sha256 || digest(item.source) !== item.sha256) {
195
+ 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.`);
196
+ }
197
+ files.push({ path: rel, sha256: item.sha256 });
198
+ }
199
+ for (const item of before) {
200
+ if (digest(item.source) !== item.sha256) {
201
+ throw new MwiError('BACKUP_UNSTABLE', `The project changed after it was copied: ${item.source}.`, `Keep ${target} for inspection and retry on an unchanged project.`);
202
+ }
203
+ }
204
+ }
205
+ catch (error) {
206
+ if (error instanceof MwiError)
207
+ throw error;
208
+ throw new MwiError('BACKUP_FAILED', error.message, `Keep ${target} for inspection; no identity was written.`);
209
+ }
210
+ const manifest = join(target, 'backup-manifest.json');
211
+ writeFileSync(manifest, JSON.stringify({ workspace, mwt, directory, files }, null, 2));
212
+ const record = {
213
+ mode: 'direct',
214
+ workspace,
215
+ mwt,
216
+ directory,
217
+ session_id: null,
218
+ backup: target,
219
+ backup_manifest: manifest,
220
+ backup_manifest_sha256: digest(manifest),
221
+ attached_at: new Date().toISOString(),
222
+ };
223
+ mkdirSync(recordRootOf(workspace), { recursive: true });
224
+ const pending = join(recordRootOf(workspace), `${id}.json.tmp`);
225
+ const final = join(recordRootOf(workspace), `${id}.json`);
226
+ writeFileSync(pending, JSON.stringify(record, null, 2));
227
+ renameSync(pending, final);
228
+ if (!valid(workspace, record)) {
229
+ 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.`);
230
+ }
231
+ return toBackup(record);
232
+ }
@@ -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.