@aryacode/dsh-engineering 0.8.2 → 0.8.4

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.4] - 2026-10-27
4
+
5
+ ### Added
6
+
7
+ - **`ftview_export` now brings the alarm setup and the tag database.** The vendor export server covers
8
+ displays, global objects and project settings; alarms and tags live beside the application in the
9
+ Studio project store and were missing from a "full" export, so tag bindings had nothing to check
10
+ against. The export now copies `Alarms/Alarms.xml` and `Tags/<application>.csv` into the export in the
11
+ same layout Studio uses, and reports a warning for each when the stored snapshot is older than the
12
+ application database (on the commissioning application both were dated 2026-09-29 against a database
13
+ modified 2026-10-27).
14
+
15
+ ### Documentation
16
+
17
+ - FTView skill, getting-started and the handbook now describe the alarms/tags copy and its staleness
18
+ warning instead of "the vendor export carries no tag CSV".
19
+ ## [0.8.3] - 2026-10-27
20
+
21
+ ### Documentation
22
+
23
+ - **A workflow handbook, generated from one source.** `docs/handbook/Engineering-Handbook.pdf` (7 pages),
24
+ an editable `.docx` and a printable `.html` cover the four products: what each row needs, the shared
25
+ read → act → verify → hand-off loop, what the agent does, what stays human, and a "what proves what"
26
+ table.
27
+ - **Six workflow diagrams** in `docs/diagrams/` (SVG masters and PNG): the shared loop and one per
28
+ product, with agent steps, human steps and proof steps visually distinguished. They are embedded in
29
+ `docs/getting-started.md` and in the handbook.
30
+ - The diagrams and handbook are **generated**, not hand-drawn: `scripts/gen-diagrams.py` owns the layout
31
+ and `scripts/gen-handbook.py` the prose, so a changed workflow is one edit and one regeneration.
32
+ - README: the handbook in the documentation table and the generators in the Development commands.
3
33
  ## [0.8.2] - 2026-10-27
4
34
 
5
35
  ### Documentation
package/README.md CHANGED
@@ -238,6 +238,7 @@ even when the implementation behind a row is replaced.
238
238
  | Document | Owns |
239
239
  |---|---|
240
240
  | [Getting started](docs/getting-started.md) | The first session: what each row needs, the shared loop, and a paste-ready prompt per product |
241
+ | [Workflow handbook (PDF)](docs/handbook/Engineering-Handbook.pdf) | The whole surface as a printable handbook with the four workflow diagrams; [editable DOCX](docs/handbook/Engineering-Handbook.docx) and [HTML](docs/handbook/Engineering-Handbook.html) beside it |
241
242
  | [Tool reference](docs/reference/tools.md) | Every tool, parameter and description (generated) |
242
243
  | [Examples](docs/examples.md) | End-to-end sessions per product |
243
244
  | [Architecture](docs/architecture.md) | Composition, row model, configuration, client faces, packaging |
@@ -256,8 +257,14 @@ pnpm build:client # regenerate lib/client.js from the committed face bundles
256
257
  pnpm test # patch, docs, tool-reference, manifest and client-face checks
257
258
  pnpm docs:tools # regenerate the tool reference from the registered definitions
258
259
  pnpm docs:check # fail if the committed reference has drifted
260
+ python scripts/gen-diagrams.py # regenerate docs/diagrams/*.svg and *.png
261
+ python scripts/gen-handbook.py # rebuild the PDF, DOCX and HTML handbook from those figures
259
262
  ```
260
263
 
264
+ The diagrams and the handbook are generated, not hand-drawn: layout lives in
265
+ `scripts/gen-diagrams.py` and the prose in `scripts/gen-handbook.py`, so a changed workflow is one edit
266
+ and one regeneration. Both need Pillow; the handbook also needs python-docx and reportlab.
267
+
261
268
  Tests run through `tsx` so they can read the TypeScript tool factories. A registered tool that no
262
269
  skill governs fails the suite — the policy is a test, not a convention.
263
270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-engineering",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",
@@ -77,7 +77,10 @@ Start from the application, not from a folder someone emailed:
77
77
 
78
78
  1. `ftview_apps` lists the ME applications installed on this host.
79
79
  2. `ftview_export` writes one to a NEW folder through the vendor export server. No Studio window is
80
- needed and the application is not modified.
80
+ needed and the application is not modified. It also copies the alarm setup (`Alarms/Alarms.xml`) and
81
+ the tag CSV (`Tags/<application>.csv`) from the Studio project store, and reports a `warning` for
82
+ either when the stored file is older than the application database — say so rather than treating
83
+ those tags or alarms as current.
81
84
  3. `ftview_open` that folder, then author against it:
82
85
  - `ftview_draft_display_create` for a new display;
83
86
  - `ftview_draft_object_add` for one object (`text`, `panel`, `momentaryButton`, `numericDisplay`,
@@ -64,10 +64,13 @@ export function vendorTools(fs) {
64
64
  if (exported.error !== undefined) {
65
65
  throw new EngineeringError('VENDOR_EXPORT_FAILED', exported.error, 'Check that the application is not open in Studio, then retry with a new output_root.');
66
66
  }
67
- const files = exported.value ?? [];
67
+ const files = exported.value?.files ?? [];
68
+ const warnings = exported.value?.warnings ?? [];
68
69
  const displays = files.filter(file => /\/Displays\/.+\.xml$/iu.test(file) && !/batchimport/iu.test(file));
69
70
  const manifests = files.filter(file => /batchimport/iu.test(file));
70
- return success(`Exported ${application}: ${files.length} file(s), ${displays.length} display(s)${manifests.length === 0 ? '' : `, ${manifests.length} import manifest(s)`}.`, { application, output_root: destination, files, display_count: displays.length, manifests }, [{ tool: 'ftview_open', args: { root: destination }, why: 'Open the fresh export and author against it.' }], VENDOR_COVERAGE);
71
+ const alarms = files.filter(file => /^Alarms\//iu.test(file));
72
+ const tags = files.filter(file => /^Tags\/.+\.csv$/iu.test(file));
73
+ return success(`Exported ${application}: ${files.length} file(s), ${displays.length} display(s)${alarms.length === 0 ? ', no alarms' : ', alarms'}${tags.length === 0 ? ', no tags CSV' : ', tags CSV'}${manifests.length === 0 ? '' : `, ${manifests.length} import manifest(s)`}${warnings.length === 0 ? '' : `; ${warnings.length} warning(s)`}.`, { application, output_root: destination, files, display_count: displays.length, alarms, tags, manifests, warnings }, [{ tool: 'ftview_open', args: { root: destination }, why: 'Open the fresh export and author against it.' }], VENDOR_COVERAGE);
71
74
  },
72
75
  }),
73
76
  ];
@@ -37,13 +37,53 @@ export interface VendorResult<T> {
37
37
  */
38
38
  export declare function listApplications(): Promise<VendorResult<readonly MeApplication[]>>;
39
39
  /**
40
- * Export one application's displays and globals into a new folder.
40
+ * Export one application's displays, globals, alarms and tags into a new folder.
41
+ *
42
+ * The export server covers displays, global objects and project settings. The
43
+ * alarm setup and the tag database live beside the application in the Studio
44
+ * project store, and `ExportAll` does not copy them, so this function does:
45
+ * without them a "full" export has no tags to bind and no alarms to review.
46
+ *
47
+ * Those two files are snapshots Studio writes when it exports them from their
48
+ * editors, not live views of the database, so each copy is checked against the
49
+ * application database's timestamp and a stale one is reported rather than
50
+ * silently handed over as current.
41
51
  *
42
52
  * @param application - Application name as listed.
43
53
  * @param destination - Folder to create and fill.
44
- * @returns Files written, relative to the destination.
54
+ * @param documentsRoot - Public Documents folder, injectable for tests.
55
+ * @returns Files written and any freshness warnings.
56
+ */
57
+ export declare function exportApplication(application: string, destination: string, documentsRoot?: string): Promise<VendorResult<ExportResult>>;
58
+ /** One export run's files and freshness warnings. */
59
+ export interface ExportResult {
60
+ /** Files written, relative to the destination. */
61
+ readonly files: readonly string[];
62
+ /** Components that are missing or older than the application database. */
63
+ readonly warnings: readonly string[];
64
+ }
65
+ /** What the project store contributed to an export. */
66
+ export interface ProjectExtras {
67
+ /** Export-relative paths copied from the store. */
68
+ readonly copied: readonly string[];
69
+ /** Why a component is missing or possibly stale. */
70
+ readonly warnings: readonly string[];
71
+ }
72
+ /** The Studio project store that holds one application's alarms and tags. */
73
+ export declare function projectFolder(application: string, documentsRoot?: string): string | undefined;
74
+ /**
75
+ * Copy the alarm setup and the tag CSV from the project store into an export.
76
+ *
77
+ * The file names and layout match what Studio writes when it exports the whole
78
+ * application (`Alarms/Alarms.xml`, `Tags/<application>.csv`), so a reader
79
+ * cannot tell this export from a hand-made one.
80
+ *
81
+ * @param application - Application name.
82
+ * @param destination - Export folder that already exists.
83
+ * @param documentsRoot - Public Documents folder, injectable for tests.
84
+ * @returns Paths copied and any staleness warnings.
45
85
  */
46
- export declare function exportApplication(application: string, destination: string): Promise<VendorResult<readonly string[]>>;
86
+ export declare function copyProjectExtras(application: string, destination: string, documentsRoot?: string): ProjectExtras;
47
87
  /**
48
88
  * Back one application up to a zip.
49
89
  *
@@ -11,7 +11,7 @@
11
11
  * @module dsh-engineering-ftview/vendor
12
12
  */
13
13
  import { spawn } from 'node:child_process';
14
- import { existsSync, readdirSync, statSync } from 'node:fs';
14
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  /** CLSID of the ME export server (`MEExport.exe`). */
17
17
  export const EXPORT_CLSID = '9d882577-aa92-466b-a749-6d6b5093b623';
@@ -88,13 +88,24 @@ export async function listApplications() {
88
88
  }
89
89
  }
90
90
  /**
91
- * Export one application's displays and globals into a new folder.
91
+ * Export one application's displays, globals, alarms and tags into a new folder.
92
+ *
93
+ * The export server covers displays, global objects and project settings. The
94
+ * alarm setup and the tag database live beside the application in the Studio
95
+ * project store, and `ExportAll` does not copy them, so this function does:
96
+ * without them a "full" export has no tags to bind and no alarms to review.
97
+ *
98
+ * Those two files are snapshots Studio writes when it exports them from their
99
+ * editors, not live views of the database, so each copy is checked against the
100
+ * application database's timestamp and a stale one is reported rather than
101
+ * silently handed over as current.
92
102
  *
93
103
  * @param application - Application name as listed.
94
104
  * @param destination - Folder to create and fill.
95
- * @returns Files written, relative to the destination.
105
+ * @param documentsRoot - Public Documents folder, injectable for tests.
106
+ * @returns Files written and any freshness warnings.
96
107
  */
97
- export async function exportApplication(application, destination) {
108
+ export async function exportApplication(application, destination, documentsRoot) {
98
109
  if (existsSync(destination))
99
110
  return { error: `A file or folder already exists at ${destination}. Export into a new folder.` };
100
111
  const result = await runPowerShell('powershell.exe', EXPORT_SCRIPT, { clsid: EXPORT_CLSID, app: application, dest: destination });
@@ -102,6 +113,7 @@ export async function exportApplication(application, destination) {
102
113
  return { error: `Export failed: ${result.stderr.trim() || `exit ${result.code}`}` };
103
114
  if (!existsSync(destination))
104
115
  return { error: 'The engine reported success but wrote no folder.' };
116
+ const extras = copyProjectExtras(application, destination, documentsRoot);
105
117
  const files = [];
106
118
  const walk = (directory, prefix) => {
107
119
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
@@ -113,7 +125,85 @@ export async function exportApplication(application, destination) {
113
125
  }
114
126
  };
115
127
  walk(destination, '');
116
- return { value: files.sort() };
128
+ return { value: { files: files.sort(), warnings: extras.warnings } };
129
+ }
130
+ /** The Studio project store that holds one application's alarms and tags. */
131
+ export function projectFolder(application, documentsRoot = join(process.env.PUBLIC ?? 'C:/Users/Public', 'Documents')) {
132
+ const folder = join(documentsRoot, 'RSView Enterprise/ME/HMI projects', application);
133
+ return existsSync(folder) ? folder : undefined;
134
+ }
135
+ /** A readable date for one file's timestamp. */
136
+ function modifiedOn(path) {
137
+ try {
138
+ return statSync(path).mtime.toISOString().slice(0, 10);
139
+ }
140
+ catch {
141
+ return 'unknown';
142
+ }
143
+ }
144
+ /** A warning when one copied snapshot predates the application database. */
145
+ function freshness(file, relative, database) {
146
+ if (database === undefined)
147
+ return undefined;
148
+ try {
149
+ if (statSync(file).mtimeMs + 60_000 >= statSync(database).mtimeMs)
150
+ return undefined;
151
+ return `${relative} was written ${modifiedOn(file)}, older than the application database (${modifiedOn(database)}); it may not reflect recent changes. Studio refreshes it on its next full export.`;
152
+ }
153
+ catch {
154
+ return undefined;
155
+ }
156
+ }
157
+ /**
158
+ * Copy the alarm setup and the tag CSV from the project store into an export.
159
+ *
160
+ * The file names and layout match what Studio writes when it exports the whole
161
+ * application (`Alarms/Alarms.xml`, `Tags/<application>.csv`), so a reader
162
+ * cannot tell this export from a hand-made one.
163
+ *
164
+ * @param application - Application name.
165
+ * @param destination - Export folder that already exists.
166
+ * @param documentsRoot - Public Documents folder, injectable for tests.
167
+ * @returns Paths copied and any staleness warnings.
168
+ */
169
+ export function copyProjectExtras(application, destination, documentsRoot) {
170
+ const source = projectFolder(application, documentsRoot);
171
+ if (source === undefined) {
172
+ return { copied: [], warnings: [`No Studio project folder was found for ${application}, so alarms and tags were not exported.`] };
173
+ }
174
+ const database = join(source, `${application}.med`);
175
+ const copied = [];
176
+ const warnings = [];
177
+ const alarms = join(source, 'Alarms.xml');
178
+ if (existsSync(alarms)) {
179
+ const target = join(destination, 'Alarms');
180
+ mkdirSync(target, { recursive: true });
181
+ copyFileSync(alarms, join(target, 'Alarms.xml'));
182
+ copied.push('Alarms/Alarms.xml');
183
+ const warning = freshness(alarms, 'Alarms/Alarms.xml', existsSync(database) ? database : undefined);
184
+ if (warning !== undefined)
185
+ warnings.push(warning);
186
+ }
187
+ else {
188
+ warnings.push('No Alarms.xml in the application store, so alarms were not exported. Export alarm setup from Studio once.');
189
+ }
190
+ // Studio names the tag file `<application>-Tags.CSV`; match the suffix
191
+ // case-insensitively because older applications used `.csv`.
192
+ const entries = readdirSync(source, { withFileTypes: true });
193
+ const tags = entries.find(entry => entry.isFile() && /-tags\.csv$/iu.test(entry.name));
194
+ if (tags !== undefined) {
195
+ const target = join(destination, 'Tags');
196
+ mkdirSync(target, { recursive: true });
197
+ copyFileSync(join(source, tags.name), join(target, `${application}.csv`));
198
+ copied.push(`Tags/${application}.csv`);
199
+ const warning = freshness(join(source, tags.name), `Tags/${application}.csv`, existsSync(database) ? database : undefined);
200
+ if (warning !== undefined)
201
+ warnings.push(warning);
202
+ }
203
+ else {
204
+ warnings.push('No tag CSV in the application store, so tags were not exported. Export the tag database from Studio once.');
205
+ }
206
+ return { copied, warnings };
117
207
  }
118
208
  /**
119
209
  * Back one application up to a zip.