@aryacode/dsh-engineering 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +7 -1
- package/cordis.patch.yml +7 -0
- package/entries/resources.js +23 -0
- package/package.json +6 -3
- package/vendor/ftview/SKILL.md +4 -1
- package/vendor/ftview/lib/edit/tools.d.ts +1 -1
- package/vendor/ftview/lib/edit/tools.js +26 -6
- package/vendor/ftview/lib/edit.js +2 -1
- package/vendor/ftview/lib/session.d.ts +9 -0
- package/vendor/ftview/lib/session.js +11 -0
- package/vendor/ftview/lib/style.d.ts +60 -0
- package/vendor/ftview/lib/style.js +109 -0
- package/vendor/ftview/lib/vendor-tools.js +5 -2
- package/vendor/ftview/lib/vendor.d.ts +43 -3
- package/vendor/ftview/lib/vendor.js +95 -5
- package/vendor/l5x/lib/engine/style.d.ts +67 -0
- package/vendor/l5x/lib/engine/style.js +119 -0
- package/vendor/l5x/lib/session.d.ts +9 -0
- package/vendor/l5x/lib/session.js +11 -0
- package/vendor/resources/SKILL.md +27 -0
- package/vendor/resources/lib/corpus.d.ts +80 -0
- package/vendor/resources/lib/corpus.js +126 -0
- package/vendor/resources/lib/index.d.ts +55 -0
- package/vendor/resources/lib/index.js +75 -0
- package/vendor/resources/lib/profile.d.ts +113 -0
- package/vendor/resources/lib/profile.js +52 -0
- package/vendor/resources/lib/store.d.ts +22 -0
- package/vendor/resources/lib/store.js +35 -0
- package/vendor/resources/lib/tools.d.ts +138 -0
- package/vendor/resources/lib/tools.js +190 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-10-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Model-agnostic engineering resources: a local standards corpus.** 34 authored pages across six domains
|
|
8
|
+
(IEC 61131-3, Rockwell, Yaskawa, FactoryTalk View/HMI, Optix, craft) with front-matter provenance and 100+
|
|
9
|
+
rule IDs (`CRAFT-NAME-01`, `VIEW-HPM-02`, …). `engineering_reference` lists the contents, ranks pages for a
|
|
10
|
+
query and returns one page with its rules; search is deterministic and needs no network, embedding model or
|
|
11
|
+
tokenizer. `scripts/gen-resources.mjs` validates every page (fields, domains, id/file agreement, rule
|
|
12
|
+
uniqueness, size and word budgets) and generates `index.json`/`SOURCE.json`; `pnpm resources:check` fails on
|
|
13
|
+
a stale index.
|
|
14
|
+
- **House-style profiles.** `engineering_profile` reads the conventions of the open project — L5X tag prefixes
|
|
15
|
+
and case, seal-in and latch forms, comment density; FactoryTalk View palette, fonts, button geometry,
|
|
16
|
+
navigation and popups — with counts, evidence and a `low`/`medium`/`high` confidence. A profile is evidence,
|
|
17
|
+
not authority, and the tool says which project and SHA-256 it came from.
|
|
18
|
+
- **Generation follows the profile.** `ftview_draft_display_create` applies the profile's background colour and
|
|
19
|
+
`ftview_draft_object_add` its button geometry and font when the caller omits them, reporting
|
|
20
|
+
`profile_applied`.
|
|
21
|
+
- **New `resources` row and the `engineering-standards` skill.** The row mounts the two tools and the skill
|
|
22
|
+
that tells a model to read the relevant page and the project profile before authoring. Tools 97 → 99, rows
|
|
23
|
+
5 → 6.
|
|
24
|
+
- `docs/resources.md` documents the corpus, its provenance, the index commands and the profile schema.
|
|
25
|
+
|
|
26
|
+
### Known limits
|
|
27
|
+
|
|
28
|
+
- `engineering_review` (rule-based draft review) and MWI/Optix profile extraction are planned for 0.9.1; the
|
|
29
|
+
corpus already applies to those products and the product validation still judges the result.
|
|
30
|
+
## [0.8.4] - 2026-10-27
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`ftview_export` now brings the alarm setup and the tag database.** The vendor export server covers
|
|
35
|
+
displays, global objects and project settings; alarms and tags live beside the application in the
|
|
36
|
+
Studio project store and were missing from a "full" export, so tag bindings had nothing to check
|
|
37
|
+
against. The export now copies `Alarms/Alarms.xml` and `Tags/<application>.csv` into the export in the
|
|
38
|
+
same layout Studio uses, and reports a warning for each when the stored snapshot is older than the
|
|
39
|
+
application database (on the commissioning application both were dated 2026-09-29 against a database
|
|
40
|
+
modified 2026-10-27).
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- FTView skill, getting-started and the handbook now describe the alarms/tags copy and its staleness
|
|
45
|
+
warning instead of "the vendor export carries no tag CSV".
|
|
3
46
|
## [0.8.3] - 2026-10-27
|
|
4
47
|
|
|
5
48
|
### Documentation
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
MotionWorks IEC and FactoryTalk Optix sit behind a single install, one row per surface. A row is the
|
|
5
5
|
switch: disable it and that plugin unloads, so its tool schemas leave the request.
|
|
6
6
|
|
|
7
|
-
`@aryacode/dsh-engineering` ·
|
|
7
|
+
`@aryacode/dsh-engineering` · 99 tools · 6 rows · 4 products · MIT
|
|
8
8
|
|
|
9
9
|
**New here? Start with [Getting started](docs/getting-started.md)** — what each row needs, the loop every
|
|
10
10
|
product follows, and the first prompt to paste for each of the four.
|
|
@@ -43,6 +43,7 @@ bundle.
|
|
|
43
43
|
| **FactoryTalk View ME** | 22 | all on | Node for an export folder; FactoryTalk View Studio ME for discovery and the handoff |
|
|
44
44
|
| **MotionWorks IEC** | 24 | all on | MotionWorks IEC 3 Pro installed; verified against 1.19 |
|
|
45
45
|
| **FactoryTalk Optix** | 10 (3 need preview) | all on | Optix Studio installed; Tesseract for OCR |
|
|
46
|
+
| **Engineering resources** | 2 | all on | none; a profile needs an L5X or FactoryTalk View project open |
|
|
46
47
|
|
|
47
48
|
Tool groups, by product:
|
|
48
49
|
|
|
@@ -51,6 +52,9 @@ Tool groups, by product:
|
|
|
51
52
|
- **FactoryTalk View** — 6 read, 5 staged edits, 3 Studio handoff
|
|
52
53
|
- **MotionWorks IEC** — 7 session, 7 saved-project reads, 3 verify, 7 direct edits
|
|
53
54
|
- **Optix** — 10 tools, linked to the `factorytalk-optix` guidance; `optix_netlogic` reads the pinned NetLogic cheat-sheet snapshot, and the 9 bridge tools need Studio
|
|
55
|
+
- **Engineering resources** — 2 tools and the `engineering-standards` skill: `engineering_reference` reads the
|
|
56
|
+
local 34-page corpus (IEC 61131-3, Rockwell, Yaskawa, HMI, Optix, craft) and `engineering_profile` infers the
|
|
57
|
+
open project's own naming, idioms and HMI palette, with evidence and a confidence level
|
|
54
58
|
|
|
55
59
|
## Switches
|
|
56
60
|
|
|
@@ -65,6 +69,7 @@ down:
|
|
|
65
69
|
| `ftview` | FactoryTalk View ME read, guidance and edits; optionally the Studio handoff | `edit` (on), `studio` (on) |
|
|
66
70
|
| `mwi` | MotionWorks IEC session, reads, verify, guidance and the edits | `workspace` (empty), `edit` (on) |
|
|
67
71
|
| `viewers` | ladder + ME display previews | — |
|
|
72
|
+
| `resources` | the engineering corpus, the house-style profile and the `engineering-standards` skill; no project of its own | `reference` (on) |
|
|
68
73
|
| `optix` | FactoryTalk Optix Studio authoring and the NetLogic reader | `bridgeUrls` (8768), `bridgeToken` (empty), `allowWrites` (on), `allowDestructive` (on), `allowInvoke` (on), `preview` (on), `allowPreviewInput` (on), `cdpUrl`, `previewUrl`, `ocrExecutable` (empty), `ocrLanguage` (`eng`), `ocrTessdataDirectory` (empty), `netlogic` (on) |
|
|
69
74
|
|
|
70
75
|
**Why the row list collapsed.** The Plugins page lists a bundle's rows flat and skips `cordis:group`
|
|
@@ -238,6 +243,7 @@ even when the implementation behind a row is replaced.
|
|
|
238
243
|
| Document | Owns |
|
|
239
244
|
|---|---|
|
|
240
245
|
| [Getting started](docs/getting-started.md) | The first session: what each row needs, the shared loop, and a paste-ready prompt per product |
|
|
246
|
+
| [Engineering resources](docs/resources.md) | The 34-page standards corpus, its rule IDs, the generated index, and the house-style profile |
|
|
241
247
|
| [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 |
|
|
242
248
|
| [Tool reference](docs/reference/tools.md) | Every tool, parameter and description (generated) |
|
|
243
249
|
| [Examples](docs/examples.md) | End-to-end sessions per product |
|
package/cordis.patch.yml
CHANGED
|
@@ -38,6 +38,13 @@
|
|
|
38
38
|
# context, so this row is independent of every product row above.
|
|
39
39
|
- id: viewers
|
|
40
40
|
name: '@aryacode/dsh-engineering'
|
|
41
|
+
# Cross-product engineering resources: a local standards corpus (IEC 61131-3,
|
|
42
|
+
# Rockwell, Yaskawa, HMI and craft rules), a house-style profile read from the
|
|
43
|
+
# open project, and the engineering-standards skill that says to read both
|
|
44
|
+
# before authoring. Disable it in a lean profile; it adds no product row.
|
|
45
|
+
- id: resources
|
|
46
|
+
name: '@aryacode/dsh-engineering/resources'
|
|
47
|
+
|
|
41
48
|
# FactoryTalk Optix: vendored like the other three products, so one install
|
|
42
49
|
# carries every surface and no component package is a dependency.
|
|
43
50
|
- id: optix
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engineering resources row: the standards corpus, the house-style profile
|
|
3
|
+
* and the `engineering-standards` skill.
|
|
4
|
+
*
|
|
5
|
+
* The corpus and the index are generated data shipped inside the vendored
|
|
6
|
+
* package; this entry only mounts them. A profile needs a product row to have a
|
|
7
|
+
* project open, and says so when none is.
|
|
8
|
+
*/
|
|
9
|
+
import * as resources from '../vendor/resources/lib/index.js'
|
|
10
|
+
|
|
11
|
+
export const name = 'engineering-resources'
|
|
12
|
+
|
|
13
|
+
/** The tools need the registry; the skill is registered when the host offers it. */
|
|
14
|
+
export const inject = ['tools']
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mount the resources surface.
|
|
18
|
+
* @param ctx - plugin context.
|
|
19
|
+
* @param config - `reference` (default true) controls the two read tools.
|
|
20
|
+
*/
|
|
21
|
+
export function apply(ctx, config = {}) {
|
|
22
|
+
resources.apply(ctx, config)
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-engineering",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -93,13 +93,16 @@
|
|
|
93
93
|
"./mwi/read": "./vendor/mwi/lib/read.js",
|
|
94
94
|
"./mwi/skill": "./vendor/mwi/lib/skill.js",
|
|
95
95
|
"./mwi/verify": "./vendor/mwi/lib/verify.js",
|
|
96
|
-
"./mwi/edit": "./vendor/mwi/lib/edit.js"
|
|
96
|
+
"./mwi/edit": "./vendor/mwi/lib/edit.js",
|
|
97
|
+
"./resources": "./entries/resources.js"
|
|
97
98
|
},
|
|
98
99
|
"scripts": {
|
|
99
|
-
"test": "node --import tsx --test test/patch.spec.mjs test/docs.spec.mjs test/tool-reference.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs test/config.spec.mjs test/netlogic.spec.mjs",
|
|
100
|
+
"test": "node --import tsx --test test/patch.spec.mjs test/docs.spec.mjs test/tool-reference.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs test/config.spec.mjs test/netlogic.spec.mjs test/resources.spec.mjs",
|
|
100
101
|
"docs:tools": "tsx scripts/gen-tool-reference.ts",
|
|
101
102
|
"docs:check": "tsx scripts/gen-tool-reference.ts --check",
|
|
102
103
|
"netlogic:sync": "node scripts/sync-netlogic.mjs",
|
|
104
|
+
"resources:build": "node scripts/resources.mjs",
|
|
105
|
+
"resources:check": "node scripts/resources.mjs --check",
|
|
103
106
|
"build:client": "node scripts/build-client.mjs",
|
|
104
107
|
"assemble": "node scripts/assemble.mjs"
|
|
105
108
|
}
|
package/vendor/ftview/SKILL.md
CHANGED
|
@@ -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`,
|
|
@@ -20,6 +20,6 @@ import type { EditOperation } from './operations.ts';
|
|
|
20
20
|
* @param maxOperations - Inclusive operations per draft.
|
|
21
21
|
* @returns Tool definitions in registration order.
|
|
22
22
|
*/
|
|
23
|
-
export declare function editTools(session: ViewSession, drafts: EditDrafts, fs: FsService, maxOperations: number): readonly ToolDefinition[];
|
|
23
|
+
export declare function editTools(session: ViewSession, drafts: EditDrafts, fs: FsService, maxOperations: number, profileOf?: (id: string) => unknown): readonly ToolDefinition[];
|
|
24
24
|
/** A helper kept for callers that want the operation count type. */
|
|
25
25
|
export type { EditOperation };
|
|
@@ -75,13 +75,13 @@ function jsonObject(value, name) {
|
|
|
75
75
|
* @param maxOperations - Inclusive operations per draft.
|
|
76
76
|
* @returns Tool definitions in registration order.
|
|
77
77
|
*/
|
|
78
|
-
export function editTools(session, drafts, fs, maxOperations) {
|
|
78
|
+
export function editTools(session, drafts, fs, maxOperations, profileOf) {
|
|
79
79
|
/** Resolve the view and append one operation. */
|
|
80
80
|
const stage = (exec, viewId, operation) => {
|
|
81
81
|
const view = session.require(viewId, exec);
|
|
82
82
|
return { view, draft: drafts.append(exec, view.id, operation, maxOperations) };
|
|
83
83
|
};
|
|
84
|
-
const staged = (draft, operation) => success(`Draft ${draft.id}: ${draft.operations.length} ${draft.operations.length === 1 ? 'operation' : 'operations'}.`, { draft_id: draft.id, operation_count: draft.operations.length, last_operation: operation }, nextValidate(draft.id), EDIT_COVERAGE);
|
|
84
|
+
const staged = (draft, operation, profileId) => success(`Draft ${draft.id}: ${draft.operations.length} ${draft.operations.length === 1 ? 'operation' : 'operations'}.`, { draft_id: draft.id, operation_count: draft.operations.length, last_operation: operation, ...(profileId === undefined ? {} : { profile_applied: profileId }) }, nextValidate(draft.id), EDIT_COVERAGE);
|
|
85
85
|
return [
|
|
86
86
|
tool({
|
|
87
87
|
name: 'ftview_draft_display_set',
|
|
@@ -182,6 +182,7 @@ export function editTools(session, drafts, fs, maxOperations) {
|
|
|
182
182
|
back_color: string('Optional background colour. Example: "#C6C6C6".'),
|
|
183
183
|
display_number: integer('Optional display number.', { minimum: 0 }),
|
|
184
184
|
file: string('Optional export-relative file path. Defaults to "Displays/<name>.xml".'),
|
|
185
|
+
profile_id: string('Optional house-style profile from engineering_profile; its background colour is used when back_color is absent.'),
|
|
185
186
|
}, ['name', 'width', 'height']),
|
|
186
187
|
async run(args, exec) {
|
|
187
188
|
const view = session.require(optionalString(args, 'view_id'), exec);
|
|
@@ -190,7 +191,9 @@ export function editTools(session, drafts, fs, maxOperations) {
|
|
|
190
191
|
throw new EngineeringError('COMPONENT_EXISTS', `A display named ${JSON.stringify(name)} already exists in this export.`, 'Pass a new display name.');
|
|
191
192
|
}
|
|
192
193
|
const displayType = optionalString(args, 'display_type');
|
|
193
|
-
const
|
|
194
|
+
const profileId = optionalString(args, 'profile_id');
|
|
195
|
+
const profile = profileId === undefined ? undefined : profileOf?.(profileId);
|
|
196
|
+
const backColor = optionalString(args, 'back_color') ?? profile?.hmi?.palette?.[0]?.value;
|
|
194
197
|
const displayNumber = optionalInteger(args, 'display_number');
|
|
195
198
|
const { draft } = stage(exec, optionalString(args, 'view_id'), {
|
|
196
199
|
kind: 'display_create',
|
|
@@ -202,7 +205,7 @@ export function editTools(session, drafts, fs, maxOperations) {
|
|
|
202
205
|
...(backColor === undefined ? {} : { backColor }),
|
|
203
206
|
...(displayNumber === undefined ? {} : { displayNumber }),
|
|
204
207
|
});
|
|
205
|
-
return staged(draft, draft.operations[draft.operations.length - 1]);
|
|
208
|
+
return staged(draft, draft.operations[draft.operations.length - 1], profileId);
|
|
206
209
|
},
|
|
207
210
|
}),
|
|
208
211
|
tool({
|
|
@@ -218,11 +221,28 @@ export function editTools(session, drafts, fs, maxOperations) {
|
|
|
218
221
|
connections_json: string('Optional connections as a JSON object, e.g. {"Value":"SystemRunning"}.'),
|
|
219
222
|
parent: string('Optional group name to add inside. Omit to add at the display root.'),
|
|
220
223
|
index: integer('Optional position among siblings, for z-order.', { minimum: 0 }),
|
|
224
|
+
profile_id: string('Optional house-style profile from engineering_profile; its button geometry and font are used when the attributes omit them.'),
|
|
221
225
|
}, ['display', 'type', 'name']),
|
|
222
226
|
async run(args, exec) {
|
|
223
227
|
const view = session.require(optionalString(args, 'view_id'), exec);
|
|
224
228
|
const display = displayOf(view, requiredString(args, 'display'));
|
|
229
|
+
const type = requiredString(args, 'type');
|
|
225
230
|
const attributes = { ...jsonObject(args.attributes_json, 'attributes_json') };
|
|
231
|
+
const profileId = optionalString(args, 'profile_id');
|
|
232
|
+
const profile = profileId === undefined ? undefined : profileOf?.(profileId);
|
|
233
|
+
if (profile !== undefined) {
|
|
234
|
+
const cluster = profile.hmi?.button?.[0]?.value;
|
|
235
|
+
if (/button$/iu.test(type) && cluster !== undefined && attributes.width === undefined && attributes.height === undefined) {
|
|
236
|
+
const [width, height] = cluster.split('x');
|
|
237
|
+
if (width !== undefined && height !== undefined) {
|
|
238
|
+
attributes.width = width;
|
|
239
|
+
attributes.height = height;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const font = profile.hmi?.fonts?.[0]?.value;
|
|
243
|
+
if (font !== undefined && attributes.fontFamily === undefined && (/text$/iu.test(type) || /button$/iu.test(type)))
|
|
244
|
+
attributes.fontFamily = font;
|
|
245
|
+
}
|
|
226
246
|
const caption = optionalString(args, 'caption');
|
|
227
247
|
if (caption !== undefined)
|
|
228
248
|
attributes.caption = caption;
|
|
@@ -232,14 +252,14 @@ export function editTools(session, drafts, fs, maxOperations) {
|
|
|
232
252
|
const { draft } = stage(exec, optionalString(args, 'view_id'), {
|
|
233
253
|
kind: 'object_add',
|
|
234
254
|
file: display.file,
|
|
235
|
-
type
|
|
255
|
+
type,
|
|
236
256
|
name: requiredString(args, 'name'),
|
|
237
257
|
attributes,
|
|
238
258
|
...(parent === undefined ? {} : { parent }),
|
|
239
259
|
...(index === undefined ? {} : { index }),
|
|
240
260
|
...(Object.keys(connections).length === 0 ? {} : { connections }),
|
|
241
261
|
});
|
|
242
|
-
return staged(draft, draft.operations[draft.operations.length - 1]);
|
|
262
|
+
return staged(draft, draft.operations[draft.operations.length - 1], profileId);
|
|
243
263
|
},
|
|
244
264
|
}),
|
|
245
265
|
tool({
|
|
@@ -30,7 +30,8 @@ export function apply(ctx, config = {}) {
|
|
|
30
30
|
if (!Number.isInteger(maxDraftOperations) || maxDraftOperations <= 0) {
|
|
31
31
|
throw new Error('ftview-edit: maxDraftOperations must be a positive integer');
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
const profiles = ctx.get?.('engineeringProfiles');
|
|
34
|
+
for (const definition of editTools(session, new EditDrafts(), stringFs(ctx.fs), maxDraftOperations, profiles?.get.bind(profiles)))
|
|
34
35
|
ctx.tools.register(definition);
|
|
35
36
|
}
|
|
36
37
|
export { EditDrafts } from "./edit/drafts.js";
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type ExecContext, type FsService } from './core.ts';
|
|
11
11
|
import { type Application } from './model.ts';
|
|
12
|
+
import { type FtviewStyleProfile } from './style.ts';
|
|
12
13
|
/** One opened export folder. */
|
|
13
14
|
export interface OpenView {
|
|
14
15
|
readonly id: string;
|
|
@@ -35,6 +36,14 @@ export declare class ViewSession {
|
|
|
35
36
|
* @returns The retained view.
|
|
36
37
|
*/
|
|
37
38
|
open(requestedRoot: string, exec?: ExecContext): Promise<OpenView>;
|
|
39
|
+
/**
|
|
40
|
+
* Read the house style of one open export.
|
|
41
|
+
*
|
|
42
|
+
* @param viewId - Explicit handle, or undefined to use the chat's only view.
|
|
43
|
+
* @param exec - Calling context.
|
|
44
|
+
* @returns The visual profile of the export.
|
|
45
|
+
*/
|
|
46
|
+
styleProfile(viewId: string | undefined, exec?: ExecContext): FtviewStyleProfile;
|
|
38
47
|
/**
|
|
39
48
|
* Resolve the view a call applies to.
|
|
40
49
|
*
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
11
|
import { EngineeringError } from "./core.js";
|
|
12
12
|
import { scanApplication } from "./model.js";
|
|
13
|
+
import { styleProfileOf } from "./style.js";
|
|
13
14
|
/** The scanned export store, keyed by chat. */
|
|
14
15
|
export class ViewSession {
|
|
15
16
|
fs;
|
|
@@ -57,6 +58,16 @@ export class ViewSession {
|
|
|
57
58
|
}
|
|
58
59
|
return view;
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the house style of one open export.
|
|
63
|
+
*
|
|
64
|
+
* @param viewId - Explicit handle, or undefined to use the chat's only view.
|
|
65
|
+
* @param exec - Calling context.
|
|
66
|
+
* @returns The visual profile of the export.
|
|
67
|
+
*/
|
|
68
|
+
styleProfile(viewId, exec) {
|
|
69
|
+
return styleProfileOf(this.require(viewId, exec).app);
|
|
70
|
+
}
|
|
60
71
|
/**
|
|
61
72
|
* Resolve the view a call applies to.
|
|
62
73
|
*
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infer the visual style of one FactoryTalk View ME export.
|
|
3
|
+
*
|
|
4
|
+
* Counts only what the export states: colours, fonts, button geometry,
|
|
5
|
+
* navigation and popup usage. Group screens inherited from another project and
|
|
6
|
+
* a two-display export are reported as low confidence rather than dressed up as
|
|
7
|
+
* a standard.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-ftview/style
|
|
10
|
+
*/
|
|
11
|
+
import type { Application } from './model.ts';
|
|
12
|
+
/** One counted value. */
|
|
13
|
+
interface Counted {
|
|
14
|
+
readonly value: string;
|
|
15
|
+
readonly count: number;
|
|
16
|
+
}
|
|
17
|
+
/** Evidence for one claim. */
|
|
18
|
+
interface Evidence {
|
|
19
|
+
readonly file?: string;
|
|
20
|
+
readonly count?: number;
|
|
21
|
+
readonly note: string;
|
|
22
|
+
}
|
|
23
|
+
/** The profile, structurally compatible with the resources schema. */
|
|
24
|
+
export interface FtviewStyleProfile {
|
|
25
|
+
readonly schema: 1;
|
|
26
|
+
readonly source: {
|
|
27
|
+
readonly kind: 'ftview';
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly sha256: string;
|
|
30
|
+
};
|
|
31
|
+
readonly naming: {
|
|
32
|
+
readonly displays: {
|
|
33
|
+
readonly case: string;
|
|
34
|
+
readonly examples: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
readonly hmi: {
|
|
38
|
+
readonly palette: readonly Counted[];
|
|
39
|
+
readonly fonts: readonly Counted[];
|
|
40
|
+
readonly button: readonly Counted[];
|
|
41
|
+
readonly navigation: {
|
|
42
|
+
readonly displaysWithNav: number;
|
|
43
|
+
readonly displays: number;
|
|
44
|
+
};
|
|
45
|
+
readonly popups: {
|
|
46
|
+
readonly onTop: number;
|
|
47
|
+
readonly replace: number;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
readonly evidence: readonly Evidence[];
|
|
51
|
+
readonly confidence: 'low' | 'medium' | 'high';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the style of one export.
|
|
55
|
+
*
|
|
56
|
+
* @param app - The scanned export.
|
|
57
|
+
* @returns The profile.
|
|
58
|
+
*/
|
|
59
|
+
export declare function styleProfileOf(app: Application): FtviewStyleProfile;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infer the visual style of one FactoryTalk View ME export.
|
|
3
|
+
*
|
|
4
|
+
* Counts only what the export states: colours, fonts, button geometry,
|
|
5
|
+
* navigation and popup usage. Group screens inherited from another project and
|
|
6
|
+
* a two-display export are reported as low confidence rather than dressed up as
|
|
7
|
+
* a standard.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-ftview/style
|
|
10
|
+
*/
|
|
11
|
+
/** The most common value in a counter, with its count. */
|
|
12
|
+
function top(counts) {
|
|
13
|
+
let best;
|
|
14
|
+
for (const [value, count] of counts) {
|
|
15
|
+
if (best === undefined || count > best.count)
|
|
16
|
+
best = { value, count };
|
|
17
|
+
}
|
|
18
|
+
return best;
|
|
19
|
+
}
|
|
20
|
+
/** Normalise a hex colour to lower case with a leading hash. */
|
|
21
|
+
function colour(value) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
const trimmed = value.trim().toLowerCase();
|
|
25
|
+
return /^#[0-9a-f]{3,8}$/u.test(trimmed) ? trimmed : undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Classify one display name's case convention. */
|
|
28
|
+
function nameCase(name) {
|
|
29
|
+
if (/^[A-Z][a-z]+(?: [A-Z][a-z]+)+$/u.test(name))
|
|
30
|
+
return 'Title Case';
|
|
31
|
+
if (/^[A-Z][A-Za-z0-9]*$/u.test(name))
|
|
32
|
+
return 'PascalCase';
|
|
33
|
+
if (/^[a-z0-9]+(?:_[a-z0-9]+)+$/u.test(name))
|
|
34
|
+
return 'snake_case';
|
|
35
|
+
return 'other';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read the style of one export.
|
|
39
|
+
*
|
|
40
|
+
* @param app - The scanned export.
|
|
41
|
+
* @returns The profile.
|
|
42
|
+
*/
|
|
43
|
+
export function styleProfileOf(app) {
|
|
44
|
+
const palette = new Map();
|
|
45
|
+
const fonts = new Map();
|
|
46
|
+
const buttons = new Map();
|
|
47
|
+
const displayCases = new Map();
|
|
48
|
+
let displaysWithNav = 0;
|
|
49
|
+
let onTop = 0;
|
|
50
|
+
let replace = 0;
|
|
51
|
+
const evidence = [];
|
|
52
|
+
for (const display of app.displays) {
|
|
53
|
+
displayCases.set(nameCase(display.name), (displayCases.get(nameCase(display.name)) ?? 0) + 1);
|
|
54
|
+
let gotoButtons = 0;
|
|
55
|
+
for (const object of display.objects) {
|
|
56
|
+
const back = colour(object.attributes.backColor);
|
|
57
|
+
if (back !== undefined)
|
|
58
|
+
palette.set(back, (palette.get(back) ?? 0) + 1);
|
|
59
|
+
const family = object.attributes.fontFamily;
|
|
60
|
+
if (family !== undefined && family.trim() !== '')
|
|
61
|
+
fonts.set(family, (fonts.get(family) ?? 0) + 1);
|
|
62
|
+
if (/button$/iu.test(object.type)) {
|
|
63
|
+
const width = object.attributes.width;
|
|
64
|
+
const height = object.attributes.height;
|
|
65
|
+
if (width !== undefined && height !== undefined) {
|
|
66
|
+
const key = `${width}x${height}`;
|
|
67
|
+
buttons.set(key, (buttons.get(key) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (object.type === 'gotoButton')
|
|
71
|
+
gotoButtons += 1;
|
|
72
|
+
if (object.type === 'displaySettings') {
|
|
73
|
+
const type = object.attributes.displayType;
|
|
74
|
+
if (type === 'onTop')
|
|
75
|
+
onTop += 1;
|
|
76
|
+
if (type === 'replace')
|
|
77
|
+
replace += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (gotoButtons >= 3)
|
|
81
|
+
displaysWithNav += 1;
|
|
82
|
+
}
|
|
83
|
+
const back = top(palette);
|
|
84
|
+
const family = top(fonts);
|
|
85
|
+
const button = top(buttons);
|
|
86
|
+
const displayCase = top(displayCases);
|
|
87
|
+
if (back !== undefined)
|
|
88
|
+
evidence.push({ count: back.count, note: `background "${back.value}" used ${back.count} time(s)` });
|
|
89
|
+
if (family !== undefined)
|
|
90
|
+
evidence.push({ count: family.count, note: `font "${family.value}" used ${family.count} time(s)` });
|
|
91
|
+
if (button !== undefined)
|
|
92
|
+
evidence.push({ count: button.count, note: `button geometry ${button.value} used ${button.count} time(s)` });
|
|
93
|
+
evidence.push({ count: app.displays.length, note: `${app.displays.length} display(s) read` });
|
|
94
|
+
const confidence = app.displays.length >= 8 && (back?.count ?? 0) >= 5 ? 'high' : app.displays.length >= 3 ? 'medium' : 'low';
|
|
95
|
+
return {
|
|
96
|
+
schema: 1,
|
|
97
|
+
source: { kind: 'ftview', path: app.root, sha256: '' },
|
|
98
|
+
naming: { displays: { case: displayCase?.value ?? 'Title Case', examples: app.displays.slice(0, 5).map(display => display.name) } },
|
|
99
|
+
hmi: {
|
|
100
|
+
palette: [...palette].sort((left, right) => right[1] - left[1]).slice(0, 6).map(([value, count]) => ({ value, count })),
|
|
101
|
+
fonts: [...fonts].sort((left, right) => right[1] - left[1]).slice(0, 4).map(([value, count]) => ({ value, count })),
|
|
102
|
+
button: [...buttons].sort((left, right) => right[1] - left[1]).slice(0, 4).map(([value, count]) => ({ value, count })),
|
|
103
|
+
navigation: { displaysWithNav, displays: app.displays.length },
|
|
104
|
+
popups: { onTop, replace },
|
|
105
|
+
},
|
|
106
|
+
evidence,
|
|
107
|
+
confidence,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
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
|
*
|