@aryacode/dsh-engineering 0.6.0 → 0.8.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 +57 -0
- package/README.md +6 -4
- package/package.json +10 -11
- package/vendor/ftview/SKILL.md +34 -5
- package/vendor/ftview/lib/display-tree.d.ts +99 -0
- package/vendor/ftview/lib/display-tree.js +306 -0
- package/vendor/ftview/lib/edit/operations.d.ts +85 -1
- package/vendor/ftview/lib/edit/operations.js +201 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +3 -0
- package/vendor/ftview/lib/edit/prepare.js +35 -1
- package/vendor/ftview/lib/edit/tools.js +249 -5
- package/vendor/ftview/lib/handoff.d.ts +58 -0
- package/vendor/ftview/lib/handoff.js +95 -0
- package/vendor/ftview/lib/index.d.ts +1 -0
- package/vendor/ftview/lib/index.js +3 -1
- package/vendor/ftview/lib/schema.d.ts +68 -0
- package/vendor/ftview/lib/schema.js +156 -0
- package/vendor/ftview/lib/vendor-tools.d.ts +17 -0
- package/vendor/ftview/lib/vendor-tools.js +74 -0
- package/vendor/ftview/lib/vendor.d.ts +56 -0
- package/vendor/ftview/lib/vendor.js +143 -0
- package/vendor/l5x/SKILL.md +27 -7
- package/vendor/l5x/lib/engine/change-set.d.ts +9 -1
- package/vendor/l5x/lib/engine/change-set.js +32 -7
- package/vendor/l5x/lib/engine/documentation.d.ts +58 -0
- package/vendor/l5x/lib/engine/documentation.js +237 -0
- package/vendor/l5x/lib/engine/instructions.d.ts +50 -0
- package/vendor/l5x/lib/engine/instructions.js +107 -0
- package/vendor/l5x/lib/engine/ladder.d.ts +74 -0
- package/vendor/l5x/lib/engine/ladder.js +209 -0
- package/vendor/l5x/lib/index.d.ts +1 -0
- package/vendor/l5x/lib/index.js +3 -1
- package/vendor/l5x/lib/model/routines.js +6 -1
- package/vendor/l5x/lib/session.d.ts +20 -0
- package/vendor/l5x/lib/session.js +36 -0
- package/vendor/l5x/lib/tools/document.d.ts +18 -0
- package/vendor/l5x/lib/tools/document.js +119 -0
- package/vendor/l5x/lib/tools/drafts.js +109 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate display XML against the installed FactoryTalk View schema.
|
|
3
|
+
*
|
|
4
|
+
* The vendor ships `Gfx-ME*.xsd` with Studio. Rather than re-implement an XSD
|
|
5
|
+
* engine, this module asks .NET (through PowerShell) to validate in process,
|
|
6
|
+
* which is exact and needs no third-party package. When the schema or the host
|
|
7
|
+
* is unavailable the caller gets a clear `schema_unavailable` result instead of
|
|
8
|
+
* a silent pass.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-engineering-ftview/schema
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/** Where Studio installs its display schemas. */
|
|
16
|
+
const SCHEMA_ROOTS = [
|
|
17
|
+
'C:/Program Files (x86)/Rockwell Software/RSView Enterprise/Schemas',
|
|
18
|
+
'C:/Program Files/Rockwell Software/RSView Enterprise/Schemas',
|
|
19
|
+
];
|
|
20
|
+
/** Coverage note shared by every schema-dependent operation. */
|
|
21
|
+
export const SCHEMA_COVERAGE = {
|
|
22
|
+
complete: false,
|
|
23
|
+
unverified: [
|
|
24
|
+
'sequence order and occurrence counts are enforced by Studio, not by this check',
|
|
25
|
+
'Studio import and runtime behaviour',
|
|
26
|
+
'semantic intent of the logic behind a display',
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
/** The schema directory, or undefined when Studio is not installed here. */
|
|
30
|
+
export function schemaDirectory() {
|
|
31
|
+
const override = process.env.FTVIEW_SCHEMA_DIR;
|
|
32
|
+
if (override !== undefined && override !== '')
|
|
33
|
+
return existsSync(override) ? override : undefined;
|
|
34
|
+
return SCHEMA_ROOTS.find(root => existsSync(root));
|
|
35
|
+
}
|
|
36
|
+
/** Every installed schema version, newest last. */
|
|
37
|
+
export function schemaVersions() {
|
|
38
|
+
const directory = schemaDirectory();
|
|
39
|
+
if (directory === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
const version = (name) => Number.parseInt(name.replace(/\D+/gu, ''), 10);
|
|
42
|
+
return readdirSync(directory)
|
|
43
|
+
.filter(name => /^Gfx-ME\d+\.xsd$/iu.test(name))
|
|
44
|
+
.sort((left, right) => version(left) - version(right));
|
|
45
|
+
}
|
|
46
|
+
/** The declared schema of one display, from `xsi:noNamespaceSchemaLocation`. */
|
|
47
|
+
export function declaredSchema(xml) {
|
|
48
|
+
const match = /xsi:noNamespaceSchemaLocation\s*=\s*"([^"]+)"/u.exec(xml);
|
|
49
|
+
return match?.[1];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Pick the schema file for one document.
|
|
53
|
+
*
|
|
54
|
+
* A display names its schema; if that file is not installed, the newest
|
|
55
|
+
* installed schema is used and the caller is told which one ran.
|
|
56
|
+
*
|
|
57
|
+
* @param xml - Display XML, used only for its declared schema.
|
|
58
|
+
* @returns Absolute schema path and its file name, or undefined.
|
|
59
|
+
*/
|
|
60
|
+
export function schemaFor(xml) {
|
|
61
|
+
const directory = schemaDirectory();
|
|
62
|
+
if (directory === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const declared = declaredSchema(xml);
|
|
65
|
+
if (declared !== undefined && existsSync(join(directory, declared)))
|
|
66
|
+
return { path: join(directory, declared), name: declared };
|
|
67
|
+
const versions = schemaVersions();
|
|
68
|
+
if (versions.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
const newest = versions[versions.length - 1];
|
|
71
|
+
return { path: join(directory, newest), name: newest };
|
|
72
|
+
}
|
|
73
|
+
/** PowerShell that validates a batch of XML strings against one schema. */
|
|
74
|
+
const VALIDATOR = `
|
|
75
|
+
$ErrorActionPreference = 'Stop'
|
|
76
|
+
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
77
|
+
$schemas = New-Object System.Xml.Schema.XmlSchemaSet
|
|
78
|
+
$null = $schemas.Add('', [string]$payload.schema)
|
|
79
|
+
$results = New-Object System.Collections.ArrayList
|
|
80
|
+
foreach ($item in $payload.items) {
|
|
81
|
+
$messages = New-Object 'System.Collections.Generic.List[string]'
|
|
82
|
+
$settings = New-Object System.Xml.XmlReaderSettings
|
|
83
|
+
$settings.ValidationType = [System.Xml.ValidationType]::Schema
|
|
84
|
+
$settings.Schemas = $schemas
|
|
85
|
+
$settings.ValidationFlags = $settings.ValidationFlags -bor [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings
|
|
86
|
+
$handler = [System.Xml.Schema.ValidationEventHandler]{ param($sender, $eventArgs) $messages.Add("$($eventArgs.Severity): $($eventArgs.Message)") }
|
|
87
|
+
$settings.add_ValidationEventHandler($handler)
|
|
88
|
+
$reader = [System.Xml.XmlReader]::Create((New-Object System.IO.StringReader([string]$item.xml)), $settings)
|
|
89
|
+
try { while ($reader.Read()) { } } catch { $messages.Add("error: $($_.Exception.Message)") } finally { $reader.Dispose() }
|
|
90
|
+
[void]$results.Add([pscustomobject]@{ file = [string]$item.file; messages = @($messages) })
|
|
91
|
+
}
|
|
92
|
+
$results | ConvertTo-Json -Depth 4 -Compress
|
|
93
|
+
`;
|
|
94
|
+
/**
|
|
95
|
+
* Validate one batch of display documents against the installed schema.
|
|
96
|
+
*
|
|
97
|
+
* @param documents - Documents to check; an empty list runs nothing.
|
|
98
|
+
* @param executable - PowerShell to use; defaults to `powershell.exe`.
|
|
99
|
+
* @returns Findings, whether validation ran, and the schema used.
|
|
100
|
+
*/
|
|
101
|
+
export async function validateSchema(documents, executable = 'powershell.exe') {
|
|
102
|
+
if (documents.length === 0)
|
|
103
|
+
return { findings: [], ran: true };
|
|
104
|
+
// Only Studio documents declare a schema. A hand-made <display> file is not
|
|
105
|
+
// one, and validating it against Gfx-ME would report a root element the
|
|
106
|
+
// fixture never claimed to be. Such files are skipped, with a reason.
|
|
107
|
+
const declared = documents.filter(document => declaredSchema(document.xml) !== undefined);
|
|
108
|
+
if (declared.length === 0) {
|
|
109
|
+
return {
|
|
110
|
+
findings: [],
|
|
111
|
+
ran: false,
|
|
112
|
+
unavailable: 'None of the changed documents declares xsi:noNamespaceSchemaLocation, so there is no vendor schema to validate against. Studio exports and authored displays always declare one.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const first = declared[0];
|
|
116
|
+
const schema = schemaFor(first.xml);
|
|
117
|
+
if (schema === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
findings: [],
|
|
120
|
+
ran: false,
|
|
121
|
+
unavailable: 'No Gfx-ME*.xsd schema is installed on this host. Install FactoryTalk View Studio, or set FTVIEW_SCHEMA_DIR to the folder that holds Gfx-ME*.xsd.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const payload = JSON.stringify({ schema: schema.path, items: declared });
|
|
125
|
+
const encoded = Buffer.from(VALIDATOR, 'utf16le').toString('base64');
|
|
126
|
+
const output = await runPowerShell(executable, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], payload);
|
|
127
|
+
if (output.code !== 0) {
|
|
128
|
+
return {
|
|
129
|
+
findings: [],
|
|
130
|
+
ran: false,
|
|
131
|
+
schema: schema.name,
|
|
132
|
+
unavailable: `The schema validator did not run: ${output.stderr.trim() || `exit ${output.code}`}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const parsed = JSON.parse(output.stdout);
|
|
136
|
+
const findings = [];
|
|
137
|
+
for (const result of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
138
|
+
for (const message of result.messages ?? []) {
|
|
139
|
+
findings.push({ file: result.file, message, error: message.startsWith('Error') || message.startsWith('error:') });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { findings, ran: true, schema: schema.name };
|
|
143
|
+
}
|
|
144
|
+
/** Run one process with stdin and collect its output. */
|
|
145
|
+
function runPowerShell(executable, args, stdin) {
|
|
146
|
+
return new Promise(resolvePromise => {
|
|
147
|
+
const child = spawn(executable, args, { windowsHide: true });
|
|
148
|
+
let stdout = '';
|
|
149
|
+
let stderr = '';
|
|
150
|
+
child.stdout?.setEncoding('utf8').on('data', chunk => { stdout += chunk; });
|
|
151
|
+
child.stderr?.setEncoding('utf8').on('data', chunk => { stderr += chunk; });
|
|
152
|
+
child.on('error', error => resolvePromise({ code: -1, stdout, stderr: stderr || error.message }));
|
|
153
|
+
child.on('close', code => resolvePromise({ code: code ?? -1, stdout, stderr }));
|
|
154
|
+
child.stdin?.end(stdin, 'utf8');
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools that reach the installed ME engine.
|
|
3
|
+
*
|
|
4
|
+
* These are the only entry points that talk to Studio's world: listing the
|
|
5
|
+
* applications it knows and exporting one to a folder. Both are read-only with
|
|
6
|
+
* respect to the project.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/vendor-tools
|
|
9
|
+
*/
|
|
10
|
+
import { type FsService, type ToolDefinition } from './core.ts';
|
|
11
|
+
/**
|
|
12
|
+
* The vendor tools.
|
|
13
|
+
*
|
|
14
|
+
* @param fs - Filesystem provider used to resolve the destination folder.
|
|
15
|
+
* @returns Tool definitions.
|
|
16
|
+
*/
|
|
17
|
+
export declare function vendorTools(fs: FsService): readonly ToolDefinition[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools that reach the installed ME engine.
|
|
3
|
+
*
|
|
4
|
+
* These are the only entry points that talk to Studio's world: listing the
|
|
5
|
+
* applications it knows and exporting one to a folder. Both are read-only with
|
|
6
|
+
* respect to the project.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-ftview/vendor-tools
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { EngineeringError, object, optionalString, requiredString, string, success, tool } from "./core.js";
|
|
12
|
+
import { exportApplication, listApplications } from "./vendor.js";
|
|
13
|
+
/** What a vendor call cannot prove. */
|
|
14
|
+
const VENDOR_COVERAGE = {
|
|
15
|
+
complete: false,
|
|
16
|
+
unverified: [
|
|
17
|
+
'the exported XML is a snapshot of the saved application, not of a running terminal',
|
|
18
|
+
'display compile and download to a PanelView terminal',
|
|
19
|
+
'Studio import behaviour',
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The vendor tools.
|
|
24
|
+
*
|
|
25
|
+
* @param fs - Filesystem provider used to resolve the destination folder.
|
|
26
|
+
* @returns Tool definitions.
|
|
27
|
+
*/
|
|
28
|
+
export function vendorTools(fs) {
|
|
29
|
+
return [
|
|
30
|
+
tool({
|
|
31
|
+
name: 'ftview_apps',
|
|
32
|
+
description: 'List the FactoryTalk View ME applications installed on this host, through the vendor export server. Requires FactoryTalk View ME on the machine.',
|
|
33
|
+
parameters: object({
|
|
34
|
+
filter: string('Case-insensitive substring matched against the application name.'),
|
|
35
|
+
}, []),
|
|
36
|
+
async run(args) {
|
|
37
|
+
const filter = (optionalString(args, 'filter') ?? '').toLowerCase();
|
|
38
|
+
const listed = await listApplications();
|
|
39
|
+
if (listed.error !== undefined) {
|
|
40
|
+
throw new EngineeringError('VENDOR_UNAVAILABLE', listed.error, 'Install FactoryTalk View ME Studio on this host, or work from an existing export folder.');
|
|
41
|
+
}
|
|
42
|
+
const apps = (listed.value ?? []).filter(app => filter === '' || app.name.toLowerCase().includes(filter));
|
|
43
|
+
return success(`${apps.length} ME application(s)${filter === '' ? '' : ` matching ${JSON.stringify(filter)}`} on this host.`, { total: apps.length, apps }, [{ tool: 'ftview_export', args: { application: apps[0]?.name ?? 'NAME', output_root: 'C:/Exports/NAME' }, why: 'Export one application to a folder this surface can read.' }], VENDOR_COVERAGE);
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
tool({
|
|
47
|
+
name: 'ftview_export',
|
|
48
|
+
description: 'Export one installed ME application (displays, global objects, tags) to a NEW folder through the vendor export server. The application is not modified.',
|
|
49
|
+
parameters: object({
|
|
50
|
+
application: string('Application name as ftview_apps lists it.'),
|
|
51
|
+
output_root: string('New folder for the export. It must not exist.'),
|
|
52
|
+
}, ['application', 'output_root']),
|
|
53
|
+
async run(args, exec) {
|
|
54
|
+
const application = requiredString(args, 'application');
|
|
55
|
+
const requested = requiredString(args, 'output_root');
|
|
56
|
+
const destination = await fs.resolve(requested, {
|
|
57
|
+
...(exec.cwd === undefined ? {} : { cwd: exec.cwd }),
|
|
58
|
+
...(exec.signal === undefined ? {} : { signal: exec.signal }),
|
|
59
|
+
});
|
|
60
|
+
if (existsSync(destination)) {
|
|
61
|
+
throw new EngineeringError('OUTPUT_EXISTS', `A file or folder already exists at ${requested}.`, 'Pass a new output_root; export never merges into an existing folder.');
|
|
62
|
+
}
|
|
63
|
+
const exported = await exportApplication(application, destination);
|
|
64
|
+
if (exported.error !== undefined) {
|
|
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
|
+
}
|
|
67
|
+
const files = exported.value ?? [];
|
|
68
|
+
const displays = files.filter(file => /\/Displays\/.+\.xml$/iu.test(file) && !/batchimport/iu.test(file));
|
|
69
|
+
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
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive the installed FactoryTalk View ME engine.
|
|
3
|
+
*
|
|
4
|
+
* Studio ships two scriptable COM servers that matter here: `MEExportSvr`
|
|
5
|
+
* (list applications, export displays) and `AppMgrServer` (backup and restore).
|
|
6
|
+
* The export server runs out of process and works from a 64-bit host; the
|
|
7
|
+
* application manager is a 32-bit in-process server, so it is reached through
|
|
8
|
+
* the 32-bit PowerShell. Nothing here edits a project: export reads, backup
|
|
9
|
+
* copies.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-engineering-ftview/vendor
|
|
12
|
+
*/
|
|
13
|
+
/** CLSID of the ME export server (`MEExport.exe`). */
|
|
14
|
+
export declare const EXPORT_CLSID = "9d882577-aa92-466b-a749-6d6b5093b623";
|
|
15
|
+
/** CLSID of the application manager (`ApplicationManagerServer.dll`). */
|
|
16
|
+
export declare const APP_MANAGER_CLSID = "4fead81a-cd72-42da-b659-f1a41499f0f9";
|
|
17
|
+
/** One installed ME application. */
|
|
18
|
+
export interface MeApplication {
|
|
19
|
+
/** Application name as Studio stores it. */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** Target platform when the engine reports one. */
|
|
22
|
+
readonly platform?: string;
|
|
23
|
+
/** Version string when the engine reports one. */
|
|
24
|
+
readonly version?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Result of one vendor call. */
|
|
27
|
+
export interface VendorResult<T> {
|
|
28
|
+
/** Parsed value on success. */
|
|
29
|
+
readonly value?: T;
|
|
30
|
+
/** Human-readable failure. */
|
|
31
|
+
readonly error?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* List every ME application the engine knows.
|
|
35
|
+
*
|
|
36
|
+
* @returns Applications, or the reason the engine could not be reached.
|
|
37
|
+
*/
|
|
38
|
+
export declare function listApplications(): Promise<VendorResult<readonly MeApplication[]>>;
|
|
39
|
+
/**
|
|
40
|
+
* Export one application's displays and globals into a new folder.
|
|
41
|
+
*
|
|
42
|
+
* @param application - Application name as listed.
|
|
43
|
+
* @param destination - Folder to create and fill.
|
|
44
|
+
* @returns Files written, relative to the destination.
|
|
45
|
+
*/
|
|
46
|
+
export declare function exportApplication(application: string, destination: string): Promise<VendorResult<readonly string[]>>;
|
|
47
|
+
/**
|
|
48
|
+
* Back one application up to a zip.
|
|
49
|
+
*
|
|
50
|
+
* @param application - Application name.
|
|
51
|
+
* @param zipPath - Destination zip; must not exist.
|
|
52
|
+
* @returns The zip path on success.
|
|
53
|
+
*/
|
|
54
|
+
export declare function backupApplication(application: string, zipPath: string): Promise<VendorResult<string>>;
|
|
55
|
+
/** Size of one file, for reports. */
|
|
56
|
+
export declare function fileSize(path: string): number;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive the installed FactoryTalk View ME engine.
|
|
3
|
+
*
|
|
4
|
+
* Studio ships two scriptable COM servers that matter here: `MEExportSvr`
|
|
5
|
+
* (list applications, export displays) and `AppMgrServer` (backup and restore).
|
|
6
|
+
* The export server runs out of process and works from a 64-bit host; the
|
|
7
|
+
* application manager is a 32-bit in-process server, so it is reached through
|
|
8
|
+
* the 32-bit PowerShell. Nothing here edits a project: export reads, backup
|
|
9
|
+
* copies.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-engineering-ftview/vendor
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/** CLSID of the ME export server (`MEExport.exe`). */
|
|
17
|
+
export const EXPORT_CLSID = '9d882577-aa92-466b-a749-6d6b5093b623';
|
|
18
|
+
/** CLSID of the application manager (`ApplicationManagerServer.dll`). */
|
|
19
|
+
export const APP_MANAGER_CLSID = '4fead81a-cd72-42da-b659-f1a41499f0f9';
|
|
20
|
+
/** Where the 32-bit PowerShell lives on this host. */
|
|
21
|
+
function powershell32() {
|
|
22
|
+
const path = join(process.env.WINDIR ?? 'C:/Windows', 'SysWOW64/WindowsPowerShell/v1.0/powershell.exe');
|
|
23
|
+
return existsSync(path) ? path : 'powershell.exe';
|
|
24
|
+
}
|
|
25
|
+
/** Run a PowerShell script with a JSON payload on stdin. */
|
|
26
|
+
function runPowerShell(executable, script, payload) {
|
|
27
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
28
|
+
return new Promise(resolvePromise => {
|
|
29
|
+
const child = spawn(executable, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], { windowsHide: true });
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
child.stdout?.setEncoding('utf8').on('data', chunk => { stdout += chunk; });
|
|
33
|
+
child.stderr?.setEncoding('utf8').on('data', chunk => { stderr += chunk; });
|
|
34
|
+
child.on('error', error => resolvePromise({ code: -1, stdout, stderr: stderr || error.message }));
|
|
35
|
+
child.on('close', code => resolvePromise({ code: code ?? -1, stdout, stderr }));
|
|
36
|
+
child.stdin?.end(JSON.stringify(payload), 'utf8');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Script that lists applications through the export server. */
|
|
40
|
+
const LIST_SCRIPT = `
|
|
41
|
+
$ErrorActionPreference = 'Stop'
|
|
42
|
+
$p = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
43
|
+
$t = [Type]::GetTypeFromCLSID([guid]$p.clsid)
|
|
44
|
+
$o = [Activator]::CreateInstance($t)
|
|
45
|
+
$names = @($o.GetMEAppNames())
|
|
46
|
+
$apps = @()
|
|
47
|
+
foreach ($name in $names) {
|
|
48
|
+
$platform = $null; $version = $null
|
|
49
|
+
try { $info = @($o.GetAppInfo([string]$name)); if ($info.Count -gt 0) { $platform = [string]$info[0] }; if ($info.Count -gt 1) { $version = [string]$info[1] } } catch { }
|
|
50
|
+
$apps += [pscustomobject]@{ name = [string]$name; platform = $platform; version = $version }
|
|
51
|
+
}
|
|
52
|
+
[pscustomobject]@{ apps = $apps } | ConvertTo-Json -Depth 4 -Compress
|
|
53
|
+
`;
|
|
54
|
+
/** Script that exports one application's displays. */
|
|
55
|
+
const EXPORT_SCRIPT = `
|
|
56
|
+
$ErrorActionPreference = 'Stop'
|
|
57
|
+
$p = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
58
|
+
$t = [Type]::GetTypeFromCLSID([guid]$p.clsid)
|
|
59
|
+
$o = [Activator]::CreateInstance($t)
|
|
60
|
+
$o.LoadMEApp([string]$p.app)
|
|
61
|
+
try { $o.ExportAll([string]$p.dest) } finally { $o.UnloadMEApp() }
|
|
62
|
+
[pscustomobject]@{ ok = $true } | ConvertTo-Json -Compress
|
|
63
|
+
`;
|
|
64
|
+
/** Script that backs one application up to a zip through the application manager. */
|
|
65
|
+
const BACKUP_SCRIPT = `
|
|
66
|
+
$ErrorActionPreference = 'Stop'
|
|
67
|
+
$p = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
68
|
+
$t = [Type]::GetTypeFromCLSID([guid]$p.clsid)
|
|
69
|
+
$o = [Activator]::CreateInstance($t)
|
|
70
|
+
$o.BackupLocal([string]$p.app, [string]$p.zip, '', 'ME')
|
|
71
|
+
[pscustomobject]@{ ok = $true } | ConvertTo-Json -Compress
|
|
72
|
+
`;
|
|
73
|
+
/**
|
|
74
|
+
* List every ME application the engine knows.
|
|
75
|
+
*
|
|
76
|
+
* @returns Applications, or the reason the engine could not be reached.
|
|
77
|
+
*/
|
|
78
|
+
export async function listApplications() {
|
|
79
|
+
const result = await runPowerShell('powershell.exe', LIST_SCRIPT, { clsid: EXPORT_CLSID });
|
|
80
|
+
if (result.code !== 0)
|
|
81
|
+
return { error: `The ME export server is unavailable: ${result.stderr.trim() || `exit ${result.code}`}` };
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(result.stdout);
|
|
84
|
+
return { value: parsed.apps ?? [] };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { error: 'The ME export server returned an unreadable answer.' };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Export one application's displays and globals into a new folder.
|
|
92
|
+
*
|
|
93
|
+
* @param application - Application name as listed.
|
|
94
|
+
* @param destination - Folder to create and fill.
|
|
95
|
+
* @returns Files written, relative to the destination.
|
|
96
|
+
*/
|
|
97
|
+
export async function exportApplication(application, destination) {
|
|
98
|
+
if (existsSync(destination))
|
|
99
|
+
return { error: `A file or folder already exists at ${destination}. Export into a new folder.` };
|
|
100
|
+
const result = await runPowerShell('powershell.exe', EXPORT_SCRIPT, { clsid: EXPORT_CLSID, app: application, dest: destination });
|
|
101
|
+
if (result.code !== 0)
|
|
102
|
+
return { error: `Export failed: ${result.stderr.trim() || `exit ${result.code}`}` };
|
|
103
|
+
if (!existsSync(destination))
|
|
104
|
+
return { error: 'The engine reported success but wrote no folder.' };
|
|
105
|
+
const files = [];
|
|
106
|
+
const walk = (directory, prefix) => {
|
|
107
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
108
|
+
const relative = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
109
|
+
if (entry.isDirectory())
|
|
110
|
+
walk(join(directory, entry.name), relative);
|
|
111
|
+
else
|
|
112
|
+
files.push(relative);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
walk(destination, '');
|
|
116
|
+
return { value: files.sort() };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Back one application up to a zip.
|
|
120
|
+
*
|
|
121
|
+
* @param application - Application name.
|
|
122
|
+
* @param zipPath - Destination zip; must not exist.
|
|
123
|
+
* @returns The zip path on success.
|
|
124
|
+
*/
|
|
125
|
+
export async function backupApplication(application, zipPath) {
|
|
126
|
+
if (existsSync(zipPath))
|
|
127
|
+
return { error: `A file already exists at ${zipPath}.` };
|
|
128
|
+
const result = await runPowerShell(powershell32(), BACKUP_SCRIPT, { clsid: APP_MANAGER_CLSID, app: application, zip: zipPath });
|
|
129
|
+
if (result.code !== 0)
|
|
130
|
+
return { error: `Backup failed: ${result.stderr.trim() || `exit ${result.code}`}` };
|
|
131
|
+
if (!existsSync(zipPath))
|
|
132
|
+
return { error: 'The application manager reported success but wrote no zip.' };
|
|
133
|
+
return { value: zipPath };
|
|
134
|
+
}
|
|
135
|
+
/** Size of one file, for reports. */
|
|
136
|
+
export function fileSize(path) {
|
|
137
|
+
try {
|
|
138
|
+
return statSync(path).size;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
package/vendor/l5x/SKILL.md
CHANGED
|
@@ -56,12 +56,12 @@ this is a backup on request ("back up this project"). Build it as a copy plus
|
|
|
56
56
|
|
|
57
57
|
## Live reads
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
`l5x-live` reads declared scalar tags over EtherNet/IP with no Studio, FactoryTalk Linx or SDK. If `poll_live_tags` is unavailable, say live reads are not enabled here.
|
|
60
60
|
|
|
61
|
-
- `poll_live_tags` takes an explicit IPv4 address, a CPU slot and 1 to 16 declared
|
|
61
|
+
- `poll_live_tags` takes an explicit IPv4 address, a CPU slot and 1 to 16 declared scalars, and returns timestamped sweeps, quality and controller identity.
|
|
62
62
|
- `start_plc_trend` samples the same way as a session-owned job the chart can follow; stop it through the job controls.
|
|
63
63
|
- These are read-only. Never write controller data, force tags, toggle bits or edit online.
|
|
64
|
-
- Report `live_logic_matches_export: "unverified"`:
|
|
64
|
+
- Report `live_logic_matches_export: "unverified"`: a read proves a value at a moment, not that the controller runs the export.
|
|
65
65
|
|
|
66
66
|
## Reporting
|
|
67
67
|
|
|
@@ -81,18 +81,38 @@ The `l5x-live` entry reads declared scalar tags straight from a controller over
|
|
|
81
81
|
The `l5x-studio` entry adds offline SDK work; it may be disabled. Call `studio_sdk_status` before using it.
|
|
82
82
|
|
|
83
83
|
- `export_acd_to_l5x` writes one new full L5X from an ACD. No controller contact.
|
|
84
|
-
- `verify_offline_l5x` imports a full L5X into a new offline ACD and builds it. Build needs project revision 37
|
|
85
|
-
- `build_acd_from_draft` imports one changed RLL or ST routine into a new ACD copy, builds
|
|
84
|
+
- `verify_offline_l5x` imports a full L5X into a new offline ACD and builds it. Build needs project revision 37+; `probe: true` tries a lower one and reports the SDK error. An L5X from `export_acd_to_l5x` may be refused as "no changes".
|
|
85
|
+
- `build_acd_from_draft` imports one changed RLL or ST routine into a new ACD copy, builds and exports it. The baseline must be an L5X exported from that ACD in this chat; neither file may change.
|
|
86
86
|
- `studio_review_evidence` checks program, routine and rung or line names and verbatim quotes before you report findings.
|
|
87
|
-
- `controller_approval_status`
|
|
87
|
+
- `controller_approval_status` says whether this session can ask a human. `capture_controller_snapshot`, `change_controller_mode` and `download_project` need per-call approval of the exact target and proceed only on `allowed-once`; `download_project` takes a `verification_id` from a passed build in this chat.
|
|
88
88
|
- `go_online`, `read_controller_mode` and `read_live_tag` are read-only and need no approval. Never write controller data, force tags, toggle bits or edit online.
|
|
89
89
|
|
|
90
90
|
## Editing
|
|
91
91
|
|
|
92
92
|
Editing uses the `draft_*` family. It may be disabled in this profile; if a draft tool is unavailable, say editing is not enabled here rather than trying another tool.
|
|
93
93
|
|
|
94
|
-
- One operation per call: `draft_rung`, `draft_st`, `draft_routine`, `draft_tag`, `draft_tag_description`, `draft_udt_member`, `draft_udt_description`.
|
|
94
|
+
- One operation per call: `draft_rung`, `draft_rung_comment`, `draft_st`, `draft_routine`, `draft_tag`, `draft_tag_description`, `draft_udt_member`, `draft_udt_description`.
|
|
95
95
|
- `draft_validate` prepares the accumulated draft against the project and returns its hash, diff summary and check findings. Read the findings before applying.
|
|
96
96
|
- `draft_apply` writes the draft to ONE new `.L5X` path. It never overwrites an existing file.
|
|
97
97
|
- After applying, reopen the written file with `open_project` to review it.
|
|
98
98
|
- Drafts belong to this chat. Another chat starts with none, and an applied draft is cleared.
|
|
99
|
+
|
|
100
|
+
## Generate
|
|
101
|
+
|
|
102
|
+
Write ladder through the composer, not by inventing NeutralText.
|
|
103
|
+
|
|
104
|
+
- `list_instructions` gives the mnemonic, category and operand slots with their kinds; read it before composing.
|
|
105
|
+
- `compose_rung` builds one rung from a tree (`spec_json`) or a pattern (`start_stop_latch`, `fault_latch`, `ton_delay`, `interlock`), resolves every operand, type-checks each slot and parses its own output; anything unknown or mistyped is refused before staging.
|
|
106
|
+
- Pass `program`, `routine`, `mode` and `rung` to stage it (plus `comment`); without them it returns text only.
|
|
107
|
+
- `fault_latch` stages two rungs at `rung` and `rung + 1`; rung numbers are not renumbered, so pick free ones.
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
`document_project` writes a handover bundle into a NEW folder and never touches the export:
|
|
112
|
+
|
|
113
|
+
- `PROJECT.md` — controller, revision, tasks, programs, counts;
|
|
114
|
+
- `io.md` / `io.csv` — modules and module-data tags;
|
|
115
|
+
- `tags.csv` and `tags-studio.csv` — every declared tag, and the `Tag Name,Description` shape Studio imports;
|
|
116
|
+
- `rungs/<program>.md` — each rung's number, comment, NeutralText, instructions and resolved tags.
|
|
117
|
+
|
|
118
|
+
The bundle is structure and citations, not prose: write the narrative yourself and use `draft_rung_comment` / `draft_tag_description` when it belongs in the project. No C&E, FAT/SAT or runtime proof.
|
|
@@ -5,6 +5,7 @@ type Operation = {
|
|
|
5
5
|
readonly routine: string;
|
|
6
6
|
readonly rung: number;
|
|
7
7
|
readonly text: string;
|
|
8
|
+
readonly comment?: string;
|
|
8
9
|
} | {
|
|
9
10
|
readonly operation: 'delete_rung';
|
|
10
11
|
readonly program: string;
|
|
@@ -17,6 +18,13 @@ type Operation = {
|
|
|
17
18
|
readonly rung: number;
|
|
18
19
|
readonly text: string;
|
|
19
20
|
readonly before_rung?: number;
|
|
21
|
+
readonly comment?: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly operation: 'set_rung_comment';
|
|
24
|
+
readonly program: string;
|
|
25
|
+
readonly routine: string;
|
|
26
|
+
readonly rung: number;
|
|
27
|
+
readonly comment: string;
|
|
20
28
|
} | {
|
|
21
29
|
readonly operation: 'replace_st';
|
|
22
30
|
readonly program: string;
|
|
@@ -59,7 +67,7 @@ type Operation = {
|
|
|
59
67
|
readonly description: string;
|
|
60
68
|
};
|
|
61
69
|
/** Compact contract returned with validation errors and embedded in tool descriptions. */
|
|
62
|
-
export declare const CHANGE_SET_HELP = "Operations (each also needs \"operation\"):\nreplace_rung {program,routine,rung,text}\ninsert_rung {program,routine,rung,text,before_rung?}\ndelete_rung {program,routine,rung}\nreplace_st {program,routine,code}\nadd_routine {program,name,language:\"RLL\"|\"ST\",code}\nadd_tag {scope,name,data_type,description,value? scalar, dimensions? \"4 2\", external_access? \"Read/Write\"|\"Read Only\"|\"None\"}\nset_tag_description {scope,name,description}\nadd_udt_member {udt,name,data_type,description}\nset_udt_description {udt,description}\nset_udt_member_description {udt,member,description}\nIdentifiers are at most 40 characters. Example: {\"version\":1,\"operations\":[{\"operation\":\"add_tag\",\"scope\":\"controller\",\"name\":\"SP_Start\",\"data_type\":\"REAL\",\"value\":80,\"description\":\"Start threshold\"}]}";
|
|
70
|
+
export declare const CHANGE_SET_HELP = "Operations (each also needs \"operation\"):\nreplace_rung {program,routine,rung,text,comment?}\ninsert_rung {program,routine,rung,text,before_rung?,comment?}\ndelete_rung {program,routine,rung}\nset_rung_comment {program,routine,rung,comment}\nreplace_st {program,routine,code}\nadd_routine {program,name,language:\"RLL\"|\"ST\",code}\nadd_tag {scope,name,data_type,description,value? scalar, dimensions? \"4 2\", external_access? \"Read/Write\"|\"Read Only\"|\"None\"}\nset_tag_description {scope,name,description}\nadd_udt_member {udt,name,data_type,description}\nset_udt_description {udt,description}\nset_udt_member_description {udt,member,description}\nIdentifiers are at most 40 characters. Example: {\"version\":1,\"operations\":[{\"operation\":\"add_tag\",\"scope\":\"controller\",\"name\":\"SP_Start\",\"data_type\":\"REAL\",\"value\":80,\"description\":\"Start threshold\"}]}";
|
|
63
71
|
/** Limits are resolved by the plugin's configuration before tools call this module. */
|
|
64
72
|
export interface ChangeSetLimits {
|
|
65
73
|
readonly operations: number;
|
|
@@ -9,12 +9,13 @@ import { uniqueRegion, xmlRegions } from "./xml-regions.js";
|
|
|
9
9
|
/** Scalar types the plugin can create with a literal initial value. */
|
|
10
10
|
const SCALAR_TYPES = new Set(['BOOL', 'SINT', 'INT', 'DINT', 'LINT', 'USINT', 'UINT', 'UDINT', 'ULINT', 'REAL', 'LREAL']);
|
|
11
11
|
/** Operation names accepted by a version-1 change set. */
|
|
12
|
-
const SUPPORTED_OPERATIONS = ['replace_rung', 'insert_rung', 'delete_rung', 'replace_st', 'add_routine', 'add_tag', 'set_tag_description', 'add_udt_member', 'set_udt_description', 'set_udt_member_description'];
|
|
12
|
+
const SUPPORTED_OPERATIONS = ['replace_rung', 'insert_rung', 'delete_rung', 'set_rung_comment', 'replace_st', 'add_routine', 'add_tag', 'set_tag_description', 'add_udt_member', 'set_udt_description', 'set_udt_member_description'];
|
|
13
13
|
/** Compact contract returned with validation errors and embedded in tool descriptions. */
|
|
14
14
|
export const CHANGE_SET_HELP = `Operations (each also needs "operation"):
|
|
15
|
-
replace_rung {program,routine,rung,text}
|
|
16
|
-
insert_rung {program,routine,rung,text,before_rung?}
|
|
15
|
+
replace_rung {program,routine,rung,text,comment?}
|
|
16
|
+
insert_rung {program,routine,rung,text,before_rung?,comment?}
|
|
17
17
|
delete_rung {program,routine,rung}
|
|
18
|
+
set_rung_comment {program,routine,rung,comment}
|
|
18
19
|
replace_st {program,routine,code}
|
|
19
20
|
add_routine {program,name,language:"RLL"|"ST",code}
|
|
20
21
|
add_tag {scope,name,data_type,description,value? scalar, dimensions? "4 2", external_access? "Read/Write"|"Read Only"|"None"}
|
|
@@ -104,17 +105,21 @@ function parseOne(value, declaredTypes) {
|
|
|
104
105
|
case 'replace_rung':
|
|
105
106
|
case 'insert_rung':
|
|
106
107
|
case 'delete_rung': {
|
|
107
|
-
fields(row, row.operation === 'delete_rung' ? ['operation', 'program', 'routine', 'rung'] : row.operation === 'insert_rung' ? ['operation', 'program', 'routine', 'rung', 'text', 'before_rung'] : ['operation', 'program', 'routine', 'rung', 'text']);
|
|
108
|
+
fields(row, row.operation === 'delete_rung' ? ['operation', 'program', 'routine', 'rung'] : row.operation === 'insert_rung' ? ['operation', 'program', 'routine', 'rung', 'text', 'before_rung', 'comment'] : ['operation', 'program', 'routine', 'rung', 'text', 'comment']);
|
|
108
109
|
const selected = { program: identifier(row, 'program'), routine: identifier(row, 'routine'), rung: integer(row, 'rung') };
|
|
109
110
|
if (row.operation === 'delete_rung')
|
|
110
111
|
return { operation: row.operation, ...selected };
|
|
111
112
|
const source = text(row, 'text');
|
|
112
113
|
if (parseLadder(source).issues.length > 0)
|
|
113
114
|
throw new EngineeringError('INVALID_LADDER', 'Rung contains malformed or unsupported NeutralText structure');
|
|
115
|
+
const comment = row.comment === undefined ? {} : { comment: text(row, 'comment') };
|
|
114
116
|
if (row.operation === 'replace_rung')
|
|
115
|
-
return { operation: row.operation, ...selected, text: source };
|
|
116
|
-
return { operation: row.operation, ...selected, text: source, ...(row.before_rung === undefined ? {} : { before_rung: integer(row, 'before_rung') }) };
|
|
117
|
+
return { operation: row.operation, ...selected, text: source, ...comment };
|
|
118
|
+
return { operation: row.operation, ...selected, text: source, ...comment, ...(row.before_rung === undefined ? {} : { before_rung: integer(row, 'before_rung') }) };
|
|
117
119
|
}
|
|
120
|
+
case 'set_rung_comment':
|
|
121
|
+
fields(row, ['operation', 'program', 'routine', 'rung', 'comment']);
|
|
122
|
+
return { operation: row.operation, program: identifier(row, 'program'), routine: identifier(row, 'routine'), rung: integer(row, 'rung'), comment: text(row, 'comment') };
|
|
118
123
|
case 'replace_st':
|
|
119
124
|
fields(row, ['operation', 'program', 'routine', 'code']);
|
|
120
125
|
return { operation: row.operation, program: identifier(row, 'program'), routine: identifier(row, 'routine'), code: text(row, 'code') };
|
|
@@ -314,7 +319,7 @@ function applyOperation(xml, operation, context) {
|
|
|
314
319
|
if (operation.operation === 'insert_rung') {
|
|
315
320
|
if (targets.length !== 0)
|
|
316
321
|
throw new EngineeringError('NAME_COLLISION', 'Rung number already exists; no automatic renumbering is performed');
|
|
317
|
-
const newRung = `<Rung Number="${operation.rung}" Type="N"
|
|
322
|
+
const newRung = `<Rung Number="${operation.rung}" Type="N">${commentXml(operation.comment)}<Text>${cdata(operation.text)}</Text></Rung>`;
|
|
318
323
|
if (operation.before_rung === undefined)
|
|
319
324
|
return insert(xml, content, newRung);
|
|
320
325
|
const before = rungs.filter(rung => Number(rung.attributes.Number) === operation.before_rung);
|
|
@@ -327,9 +332,29 @@ function applyOperation(xml, operation, context) {
|
|
|
327
332
|
const target = targets[0];
|
|
328
333
|
if (operation.operation === 'delete_rung')
|
|
329
334
|
return replace(xml, target.start, target.end, '');
|
|
335
|
+
if (operation.operation === 'set_rung_comment') {
|
|
336
|
+
return replace(xml, target.start, target.end, rungWith(target, retainedText(xml, target), operation.comment));
|
|
337
|
+
}
|
|
338
|
+
if (operation.comment !== undefined) {
|
|
339
|
+
return replace(xml, target.start, target.end, rungWith(target, `<Text>${cdata(operation.text)}</Text>`, operation.comment));
|
|
340
|
+
}
|
|
330
341
|
const contentText = uniqueRegion(target.children, 'Text');
|
|
331
342
|
return replace(xml, contentText.start, contentText.end, `<Text>${cdata(operation.text)}</Text>`);
|
|
332
343
|
}
|
|
344
|
+
/** A comment element, or nothing when the comment is cleared. */
|
|
345
|
+
function commentXml(comment) {
|
|
346
|
+
return comment === undefined || comment === '' ? '' : `<Comment>${cdata(comment)}</Comment>`;
|
|
347
|
+
}
|
|
348
|
+
/** The rung's existing `<Text>` element, verbatim, for a comment-only edit. */
|
|
349
|
+
function retainedText(xml, rung) {
|
|
350
|
+
const text = rung.children.find(child => child.kind === 'Text');
|
|
351
|
+
return text === undefined ? '<Text><![CDATA[]]></Text>' : xml.slice(text.start, text.end);
|
|
352
|
+
}
|
|
353
|
+
/** Rebuild one rung element with a comment and a text element. */
|
|
354
|
+
function rungWith(rung, text, comment) {
|
|
355
|
+
const type = rung.attributes.Type ?? 'N';
|
|
356
|
+
return `<Rung Number="${rung.attributes.Number}" Type="${type}">${commentXml(comment)}${text}</Rung>`;
|
|
357
|
+
}
|
|
333
358
|
/** Every type name a change may reference: scalars plus declared types. */
|
|
334
359
|
function declaredTypeNames(project) {
|
|
335
360
|
const names = new Set([...SCALAR_TYPES].map(name => name.toLowerCase()));
|