@aryacode/dsh-engineering 0.4.20 → 0.4.22

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 CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.22] - 2026-10-27
4
+
5
+ ### Fixed
6
+
7
+ - **No Python bytecode in the tarball, again.** 0.4.21 shipped three `__pycache__/*.pyc` files
8
+ because a `py_compile` guard check ran in the checkout before packing, and `assemble.mjs` copied
9
+ `code/` wholesale. They were inert — the interpreter ignores a `.pyc` whose source no longer matches
10
+ — but a stale `staging.cpython-312.pyc` beside a changed guard is exactly the ambiguity this
11
+ release line must not carry. `assemble.mjs` now excludes `__pycache__` and `*.pyc` at copy time, so
12
+ the outcome no longer depends on someone remembering to delete them.
13
+
14
+ ## [0.4.21] - 2026-10-27
15
+
16
+ ### Changed
17
+
18
+ - **One backup tree for every engineering product.** Backups land in
19
+ `<workspace>/backups/<product>/<subject>/<UTC stamp>/` with a `backup-manifest.json` naming every
20
+ copied file and its SHA-256: `motionworks` for the `.mwt` and its expanded folder, `l5x` for the ACD
21
+ or L5X, `ftview` for an export folder, `optix` for a Studio project export. Studio 5000 and
22
+ FactoryTalk View take one on request (their writes already produce a new file or folder); Optix has
23
+ the location but no automated action here, because its tree is the prebuilt component. Every skill
24
+ states the same tree and a test fails when one drifts. `<workspace>/_backups` and the pre-0.4.21
25
+ `<workspace>/backups/<project>/<stamp>/` layout are read-only history and are never written.
26
+ - **MotionWorks moves its copies into that tree.** `mw_project_open` writes
27
+ `backups/motionworks/<project>/<stamp>/`, and both the bridge (`Get-DirectIdentity`) and the engine
28
+ (`assert_direct_attached`) accept a record that points at the old `.motionworks/attached/backups`
29
+ root, so an existing workspace keeps its verified backup with no migration and no second copy.
30
+
31
+ ### Fixed
32
+
33
+ - **`pnpm pack` can no longer ship a stale component.** `scripts/assemble.mjs` refuses a component
34
+ whose `lib/` is older than its `src/`, and the release procedure builds the components before
35
+ packing. That is how the 0.4.20 market tarball came to carry the 0.4.16 MotionWorks build under a
36
+ 0.4.20 version.
37
+
3
38
  ## [0.4.20] - 2026-10-27
4
39
 
5
40
  ### Fixed
package/README.md CHANGED
@@ -180,7 +180,13 @@ even when the implementation behind a row is replaced.
180
180
  - **Edits write new files.** `draft_apply` writes one new L5X beside the source and refuses to
181
181
  overwrite; `ftview_apply` writes a new export folder and leaves the source untouched. MotionWorks
182
182
  edits write the real workspace project, after one complete backup under
183
- `<workspace>/.motionworks/attached/`, and verify unrelated saved and native state.
183
+ `<workspace>/backups/motionworks/`, and verify unrelated saved and native state.
184
+ - **One backup tree.** Every product backs up into
185
+ `<workspace>/backups/<product>/<subject>/<UTC stamp>/` with a `backup-manifest.json` naming each
186
+ copied file and its SHA-256, so "back up this project" means the same place in every skill.
187
+ MotionWorks takes its backup on open; L5X, FactoryTalk View and Optix take one on request and before
188
+ an in-place change. `<workspace>/_backups` and `<workspace>/backups/<project>/…` are older layouts
189
+ and are never written.
184
190
  - **Controller changes need a human.** Upload, mode change and download require per-call approval and
185
191
  a session policy that permits asking. The tools refuse before contacting a controller when approval
186
192
  is unavailable, and a download accepts only a verification id produced by a passed build.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-engineering",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "AryaCode engineering bundle: one install surface for the Studio 5000 L5X, FactoryTalk View ME, MotionWorks IEC and FactoryTalk Optix surfaces, one row per product, with the ladder and display previews.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -13,6 +13,23 @@ A staged edit is written to a NEW export folder; the source is never modified. T
13
13
  a manual import for a human; it does not drive Studio, contact a terminal, or decode MER or MED
14
14
  databases.
15
15
 
16
+ ## Backups
17
+
18
+ Every engineering product backs up into the same tree, so a backup means one place whichever surface
19
+ you are in:
20
+
21
+ ```
22
+ <workspace>/backups/<product>/<subject>/<UTC stamp>/
23
+ backup-manifest.json every copied file: relative path + SHA-256
24
+ <the copy>
25
+ ```
26
+
27
+ FactoryTalk View is the `ftview` product: the copy is the whole export folder (display XML plus the tag
28
+ CSV), one stamp per backup. `ftview_apply` already writes a new folder, so this is a backup on request
29
+ ("back up this export"). Build it as a copy plus `backup-manifest.json` listing every file's relative
30
+ path and SHA-256. `<workspace>/_backups` and `<workspace>/backups/<project>/…` are older layouts: never
31
+ write there, and leave what is there alone.
32
+
16
33
  ## Start
17
34
 
18
35
  1. Call `ftview_open` with the export folder. Keep the returned `view_id`.
@@ -10,6 +10,23 @@ whenToUse: Use when the user asks about a Studio 5000 L5X export, Logix tags, ro
10
10
 
11
11
  This surface reads and drafts. A draft writes only a new L5X file: it never overwrites the source, imports into Studio, connects to a controller, changes mode, or writes runtime data.
12
12
 
13
+ ## Backups
14
+
15
+ Every engineering product backs up into the same tree, so a backup means one place whichever surface
16
+ you are in:
17
+
18
+ ```
19
+ <workspace>/backups/<product>/<subject>/<UTC stamp>/
20
+ backup-manifest.json every copied file: relative path + SHA-256
21
+ <the copy>
22
+ ```
23
+
24
+ Studio 5000 is the `l5x` product: the copy holds the `.ACD` being worked on, or the `.L5X` export when
25
+ that is the source of truth, one stamp per backup. Drafts and SDK builds already write new files, so
26
+ this is a backup on request ("back up this project"). Build it as a copy plus
27
+ `backup-manifest.json` listing every file's relative path and SHA-256. `<workspace>/_backups` and
28
+ `<workspace>/backups/<project>/…` are older layouts: never write there, and leave what is there alone.
29
+
13
30
  ## Start
14
31
 
15
32
  1. Call `open_project` with the export path. Keep the returned `project_id`.
@@ -2,14 +2,16 @@
2
2
 
3
3
  MotionWorks IEC 3 Pro tools, redesigned for the `dsh-engineering` bundle. The product edits the
4
4
  **real workspace project**: `mw_project_open` writes one complete hash-verified backup under
5
- `<workspace>/.motionworks/attached/` first, then drives the running IDE through the existing 32-bit
6
- COM bridge. A project the IDE already has open is attached (backup recorded) instead of reloaded, so
7
- unsaved editor changes survive. `mw_project_stage` remains as an optional sandbox.
5
+ `<workspace>/backups/motionworks/<project>/<stamp>/` first, then drives the running IDE through the
6
+ existing 32-bit COM bridge. A project the IDE already has open is attached (backup recorded) instead of
7
+ reloaded, so unsaved editor changes survive. `mw_project_stage` remains as an optional sandbox.
8
8
 
9
- The backup layout is a contract, not a convention: the shipped bridge (`Get-DirectIdentity`) and the
10
- read-only engine (`assert_direct_attached`) refuse a real project unless an identity record names a
11
- backup under `.motionworks/attached/backups` with a manifest whose SHA-256 matches. Writing the copy
12
- anywhere else is what made every direct open fail.
9
+ `<workspace>/backups/<product>/<subject>/<stamp>/` is the one tree every engineering product writes
10
+ under, so a backup is one place whichever surface the user is in. MotionWorks is the product whose
11
+ backup is also a contract: the shipped bridge (`Get-DirectIdentity`) and the read-only engine
12
+ (`assert_direct_attached`) refuse a real project unless an identity record names a copy in that tree,
13
+ with a manifest whose SHA-256 matches. Records written before 0.4.21 that point at
14
+ `.motionworks/attached/backups` stay valid.
13
15
 
14
16
  ## Rows
15
17
 
@@ -33,7 +35,7 @@ Edit tools: `mw_edit_code`, `mw_edit_variables`, `mw_edit_structure`, `mw_insert
33
35
 
34
36
  Verified against MotionWorks IEC 3 Pro 1.19 with a copy of a real project in a temporary workspace:
35
37
 
36
- 1. `open` writes `.motionworks/attached/backups/<id>/` with `backup-manifest.json` and
38
+ 1. `open` writes `backups/motionworks/<project>/<stamp>/` with `backup-manifest.json` and
37
39
  `.motionworks/attached/records/<id>.json`, verified before the IDE is asked to open anything, and
38
40
  reuses that backup on later attaches.
39
41
  2. A project already open in the IDE is attached without an `OpenProject` call: `mw_project_open`
@@ -9,12 +9,25 @@ whenToUse: The user has MotionWorks IEC 3 Pro open or asks for work in it — a
9
9
  ## Boundary
10
10
 
11
11
  This surface edits the **real workspace project**. `mw_project_open` takes one complete, hash-verified
12
- backup under `<workspace>/.motionworks/attached/` before the IDE opens the project, and reuses that
12
+ backup under `<workspace>/backups/motionworks/` before the IDE opens the project, and reuses that
13
13
  backup for later attaches. A project the IDE already has open is attached — the backup is written and
14
14
  the project is **not** reloaded, so unsaved editor changes survive. `mw_project_stage` remains
15
15
  available as an optional sandbox when you want the IDE to open a copy instead. Nothing here downloads
16
16
  to a controller or commands motion.
17
17
 
18
+ Every engineering product backs up into the same tree, so a backup means one place whichever surface
19
+ you are in:
20
+
21
+ ```
22
+ <workspace>/backups/<product>/<subject>/<UTC stamp>/
23
+ backup-manifest.json every copied file: relative path + SHA-256
24
+ <the copy>
25
+ ```
26
+
27
+ MotionWorks is the `motionworks` product and the copy holds the project's `.mwt` beside its expanded
28
+ folder. The open already takes it; do not take a second one. `<workspace>/_backups` and
29
+ `<workspace>/backups/<project>/…` are older layouts: never write there, and leave what is there alone.
30
+
18
31
  The native IDE API is the editing path. Desktop input belongs to the computer-use companion and is a
19
32
  fallback for operations without a native call, never a substitute for a refused native one.
20
33
 
@@ -1019,7 +1019,11 @@ function Get-DirectIdentity([string]$path) {
1019
1019
  try { $raw = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json }
1020
1020
  catch { continue }
1021
1021
  if ([string]$raw.mode -cne 'direct' -or -not (Test-SameProject ([string]$raw.mwt) $path)) { continue }
1022
- $backupRoot = Normalize-MwPath (Join-Path $script:WorkspaceRoot '.motionworks\attached\backups')
1022
+ # One workspace backup tree for every engineering product:
1023
+ # <workspace>\backups\<product>\<subject>\<stamp>. Records written before
1024
+ # 0.4.21 point at .motionworks\attached\backups and stay valid.
1025
+ $backupRoot = Normalize-MwPath (Join-Path $script:WorkspaceRoot 'backups\motionworks')
1026
+ $legacyRoot = Normalize-MwPath (Join-Path $script:WorkspaceRoot '.motionworks\attached\backups')
1023
1027
  $backup = Normalize-MwPath ([string]$raw.backup)
1024
1028
  $manifestPath = Normalize-MwPath ([string]$raw.backup_manifest)
1025
1029
  if (-not (Test-InsideWorkspace $backup) -or -not (Test-InsideWorkspace $manifestPath)) { return $null }
@@ -1029,7 +1033,8 @@ function Get-DirectIdentity([string]$path) {
1029
1033
  -not (Test-SameProject ([string]$raw.directory) $path) -or
1030
1034
  -not (Test-Path -LiteralPath ([string]$raw.mwt)) -or
1031
1035
  -not (Test-Path -LiteralPath ([string]$raw.directory)) -or
1032
- -not $backup.StartsWith($backupRoot + '\', [StringComparison]::OrdinalIgnoreCase) -or
1036
+ -not ($backup.StartsWith($backupRoot + '\', [StringComparison]::OrdinalIgnoreCase) -or
1037
+ $backup.StartsWith($legacyRoot + '\', [StringComparison]::OrdinalIgnoreCase)) -or
1033
1038
  -not $manifestPath.StartsWith($backup + '\', [StringComparison]::OrdinalIgnoreCase) -or
1034
1039
  -not (Test-Path -LiteralPath ([string]$raw.backup_manifest))) { return $null }
1035
1040
  if ((Get-MwSha256 ([string]$raw.backup_manifest)) -ine [string]$raw.backup_manifest_sha256) { return $null }
@@ -149,9 +149,13 @@ def assert_direct_attached(project_root: Path | str) -> Path:
149
149
  continue
150
150
  backup = Path(record["backup"]).resolve()
151
151
  manifest = Path(record["backup_manifest"]).resolve()
152
- backup_root = workspace / ".motionworks" / "attached" / "backups"
152
+ # One workspace backup tree for every engineering product:
153
+ # <workspace>/backups/<product>/<subject>/<stamp>. Records written
154
+ # before 0.4.21 point at .motionworks/attached/backups and stay valid.
155
+ backup_root = workspace / "backups" / "motionworks"
156
+ legacy_root = workspace / ".motionworks" / "attached" / "backups"
153
157
  if (Path(record["workspace"]).resolve() != workspace
154
- or not _inside(backup, backup_root)
158
+ or not (_inside(backup, backup_root) or _inside(backup, legacy_root))
155
159
  or not _inside(manifest, backup)
156
160
  or not manifest.is_file()
157
161
  or hashlib.sha256(manifest.read_bytes()).hexdigest() != record["backup_manifest_sha256"]
@@ -3,13 +3,18 @@
3
3
  *
4
4
  * Editing happens on the real workspace project. The first write takes one
5
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`.
6
+ * `<workspace>/backups/motionworks/<project>/<UTC stamp>`, writes a manifest
7
+ * beside it and records the project identity in
8
+ * `<workspace>/.motionworks/attached/records`.
8
9
  *
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.
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.
13
18
  *
14
19
  * @module dsh-engineering-mwi/session/backup
15
20
  */
@@ -37,8 +42,20 @@ export interface BackupRecord {
37
42
  /** When the copy was written. */
38
43
  readonly created_at: string;
39
44
  }
40
- /** The shared backup root of one workspace. */
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
+ */
41
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;
42
59
  /** The identity record directory of one workspace. */
43
60
  export declare function recordRootOf(workspace: string): string;
44
61
  /** Read the record for a project, when a valid backup exists. */
@@ -3,13 +3,18 @@
3
3
  *
4
4
  * Editing happens on the real workspace project. The first write takes one
5
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`.
6
+ * `<workspace>/backups/motionworks/<project>/<UTC stamp>`, writes a manifest
7
+ * beside it and records the project identity in
8
+ * `<workspace>/.motionworks/attached/records`.
8
9
  *
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.
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.
13
18
  *
14
19
  * @module dsh-engineering-mwi/session/backup
15
20
  */
@@ -18,8 +23,22 @@ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSy
18
23
  import { basename, dirname, join, relative, resolve } from 'node:path';
19
24
  import { MwiError } from "../core/errors.js";
20
25
  import { isInside } from "./workspace.js";
21
- /** The shared backup root of one workspace. */
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
+ */
22
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) {
23
42
  return join(workspace, '.motionworks', 'attached', 'backups');
24
43
  }
25
44
  /** The identity record directory of one workspace. */
@@ -66,7 +85,11 @@ function valid(workspace, record) {
66
85
  return false;
67
86
  if (!same(record.directory, record.mwt.slice(0, -4)))
68
87
  return false;
69
- if (!isInside(backupRootOf(workspace), record.backup) || !isInside(record.backup, record.backup_manifest))
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))
70
93
  return false;
71
94
  if (!existsSync(record.mwt) || !existsSync(record.directory) || !existsSync(record.backup_manifest))
72
95
  return false;
@@ -179,7 +202,9 @@ export function ensureBackup(workspace, path) {
179
202
  }
180
203
  }
181
204
  const id = randomUUID();
182
- const target = join(backupRootOf(workspace), id);
205
+ const name = basename(mwt, '.mwt');
206
+ const stamp = new Date().toISOString().replace(/[:.]/gu, '-');
207
+ const target = join(backupRootOf(workspace), name, stamp);
183
208
  const files = [];
184
209
  const sources = [mwt];
185
210
  tree(directory, sources);