@aryacode/dsh-engineering 0.6.0 → 0.7.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 +32 -0
- package/README.md +4 -3
- package/package.json +10 -11
- 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,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The documentation and generation-reference tools.
|
|
3
|
+
*
|
|
4
|
+
* `list_instructions` is the catalog the composer validates against, exposed so a
|
|
5
|
+
* caller can read operand order instead of guessing it. `document_project` builds
|
|
6
|
+
* the handover bundle into a new folder: it writes documents, never the project.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-engineering-l5x/tools/document
|
|
9
|
+
*/
|
|
10
|
+
import { DOCUMENT_PARTS, buildDocumentation } from "../engine/documentation.js";
|
|
11
|
+
import { INSTRUCTIONS } from "../engine/instructions.js";
|
|
12
|
+
import { L5xError } from "../core/errors.js";
|
|
13
|
+
import { success } from "../core/result.js";
|
|
14
|
+
import { choice, integer, list, object, string } from "../core/params.js";
|
|
15
|
+
import { PROJECT_ID, optionalString, requiredString, tool } from "./common.js";
|
|
16
|
+
/** What a catalog listing cannot establish. */
|
|
17
|
+
const CATALOG_COVERAGE = {
|
|
18
|
+
complete: false,
|
|
19
|
+
unverified: [
|
|
20
|
+
'vendor instructions outside this catalog',
|
|
21
|
+
'operand coercion the controller applies at compile time',
|
|
22
|
+
'runtime behavior',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
/** What a generated document bundle cannot establish. */
|
|
26
|
+
const DOCUMENT_COVERAGE = {
|
|
27
|
+
complete: false,
|
|
28
|
+
unverified: [
|
|
29
|
+
'narrative prose: the bundle carries structure, citations and existing descriptions only',
|
|
30
|
+
'cause-and-effect and FAT/SAT documents are not generated',
|
|
31
|
+
'runtime behavior and anything outside the export (HMI, produced/consumed tags, drivers)',
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
/** One catalog row as the tool returns it. */
|
|
35
|
+
function catalogRow(instruction) {
|
|
36
|
+
return {
|
|
37
|
+
name: instruction.name,
|
|
38
|
+
category: instruction.category,
|
|
39
|
+
summary: instruction.summary,
|
|
40
|
+
operands: instruction.operands.map(slot => ({ name: slot.name, kind: slot.kind, required: slot.optional !== true })),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The documentation tools.
|
|
45
|
+
*
|
|
46
|
+
* @param session - Project handles for the calling chat.
|
|
47
|
+
* @returns Tool definitions in registration order.
|
|
48
|
+
*/
|
|
49
|
+
export function documentTools(session) {
|
|
50
|
+
return [
|
|
51
|
+
tool({
|
|
52
|
+
name: 'list_instructions',
|
|
53
|
+
description: 'List the ladder instructions the composer validates: mnemonic, category, summary and operand slots with their kinds. Read it before composing a rung instead of guessing operand order.',
|
|
54
|
+
parameters: object({
|
|
55
|
+
filter: string('Case-insensitive substring matched against the instruction name, category or summary.'),
|
|
56
|
+
category: string('Only instructions in this category, for example "Comparison".'),
|
|
57
|
+
limit: integer('Maximum rows to return.', { minimum: 1, maximum: 200, default: 100 }),
|
|
58
|
+
}, []),
|
|
59
|
+
async run(args) {
|
|
60
|
+
const filter = (optionalString(args, 'filter') ?? '').toLowerCase();
|
|
61
|
+
const category = (optionalString(args, 'category') ?? '').toLowerCase();
|
|
62
|
+
const limit = typeof args.limit === 'number' ? Math.min(Math.max(Math.trunc(args.limit), 1), 200) : 100;
|
|
63
|
+
const rows = INSTRUCTIONS
|
|
64
|
+
.filter(instruction => category === '' || instruction.category.toLowerCase() === category)
|
|
65
|
+
.filter(instruction => filter === '' || instruction.name.toLowerCase().includes(filter)
|
|
66
|
+
|| instruction.category.toLowerCase().includes(filter)
|
|
67
|
+
|| instruction.summary.toLowerCase().includes(filter));
|
|
68
|
+
return success(`${rows.length} instruction(s)${category === '' ? '' : ` in ${category}`}${filter === '' ? '' : ` matching ${JSON.stringify(filter)}`}; showing ${Math.min(rows.length, limit)}.`, {
|
|
69
|
+
total: rows.length,
|
|
70
|
+
categories: [...new Set(INSTRUCTIONS.map(instruction => instruction.category))],
|
|
71
|
+
rows: rows.slice(0, limit).map(catalogRow),
|
|
72
|
+
}, [{ tool: 'compose_rung', why: 'Compose a rung from the instruction and its operand order.' }], CATALOG_COVERAGE);
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
tool({
|
|
76
|
+
name: 'document_project',
|
|
77
|
+
description: 'Write a documentation bundle for the open project into a NEW folder: project summary, I/O list, tag CSVs (including the Studio Tags and Comments shape) and per-program rung documentation with comments and referenced tags. Never writes to the export.',
|
|
78
|
+
parameters: object({
|
|
79
|
+
output_root: string('New folder for the bundle. It must not exist. Example: "C:/Projects/Line/documentation".'),
|
|
80
|
+
include: list('Optional parts to produce; default all. One of project, io, tags, rungs.', choice('One part.', DOCUMENT_PARTS)),
|
|
81
|
+
programs: list('Optional program names to restrict the bundle to.', string('Program name.')),
|
|
82
|
+
routines: list('Optional routine names to restrict the rung documentation to.', string('Routine name.')),
|
|
83
|
+
rungs_per_file: integer('Most rungs per rung-documentation file before a continuation file is written. Defaults to 200.', { minimum: 1, maximum: 2000 }),
|
|
84
|
+
project_id: PROJECT_ID,
|
|
85
|
+
}, ['output_root']),
|
|
86
|
+
async run(args, exec) {
|
|
87
|
+
const project = await session.require(optionalString(args, 'project_id'), exec);
|
|
88
|
+
const outputRoot = requiredString(args, 'output_root');
|
|
89
|
+
const include = Array.isArray(args.include) && args.include.length > 0
|
|
90
|
+
? args.include.map(String)
|
|
91
|
+
: undefined;
|
|
92
|
+
const unknown = include?.filter(part => !DOCUMENT_PARTS.includes(part));
|
|
93
|
+
if (unknown !== undefined && unknown.length > 0) {
|
|
94
|
+
throw new L5xError('INVALID_ARGUMENT', `Unknown part(s): ${unknown.join(', ')}.`, `Pass any of: ${DOCUMENT_PARTS.join(', ')}.`);
|
|
95
|
+
}
|
|
96
|
+
if (await session.exists(outputRoot, exec)) {
|
|
97
|
+
throw new L5xError('OUTPUT_EXISTS', `A file or folder already exists at ${outputRoot}.`, 'Pass a new output_root; the generator never merges into an existing folder.');
|
|
98
|
+
}
|
|
99
|
+
const bundle = buildDocumentation(project, {
|
|
100
|
+
...(include === undefined ? {} : { include }),
|
|
101
|
+
...(Array.isArray(args.programs) ? { programs: args.programs.map(String) } : {}),
|
|
102
|
+
...(Array.isArray(args.routines) ? { routines: args.routines.map(String) } : {}),
|
|
103
|
+
...(typeof args.rungs_per_file === 'number' ? { rungsPerFile: args.rungs_per_file } : {}),
|
|
104
|
+
});
|
|
105
|
+
const written = [];
|
|
106
|
+
for (const [path, text] of bundle.files) {
|
|
107
|
+
const destination = await session.writeTextAt(`${outputRoot.replace(/[\\/]+$/u, '')}/${path}`, text, exec);
|
|
108
|
+
written.push({ path: destination, bytes: new TextEncoder().encode(text).length });
|
|
109
|
+
}
|
|
110
|
+
return success(`Wrote ${written.length} file(s) for ${bundle.counts.programs} program(s), ${bundle.counts.routines} routine(s), ${bundle.counts.rungs} rung(s) (${bundle.counts.comments} commented) to ${outputRoot}.`, {
|
|
111
|
+
output_root: outputRoot,
|
|
112
|
+
files: written,
|
|
113
|
+
counts: bundle.counts,
|
|
114
|
+
excluded: bundle.excluded,
|
|
115
|
+
}, [{ tool: 'draft_rung_comment', why: 'Write the approved narrative back as rung comments, then apply the draft.' }], DOCUMENT_COVERAGE);
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @module dsh-engineering-l5x/tools/drafts
|
|
10
10
|
*/
|
|
11
11
|
import { prepareChangeSet } from "../engine/change-set.js";
|
|
12
|
+
import { composePattern, composeRung, PATTERNS } from "../engine/ladder.js";
|
|
12
13
|
import { L5xError } from "../core/errors.js";
|
|
13
14
|
import { success } from "../core/result.js";
|
|
14
15
|
import { choice, integer, object, string } from "../core/params.js";
|
|
@@ -26,6 +27,30 @@ const DRAFT_COVERAGE = {
|
|
|
26
27
|
function requestOf(draft) {
|
|
27
28
|
return JSON.stringify({ version: 1, operations: draft.operations });
|
|
28
29
|
}
|
|
30
|
+
/** What a composed rung does not prove. */
|
|
31
|
+
const COMPOSED_COVERAGE = {
|
|
32
|
+
complete: false,
|
|
33
|
+
unverified: [
|
|
34
|
+
'compiled behaviour: the composer validates instruction names, operand counts and operand types only',
|
|
35
|
+
'Studio 5000 import and build are not performed by this surface',
|
|
36
|
+
'runtime behavior',
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
/** Parse one JSON-object argument, refusing anything else. */
|
|
40
|
+
function jsonObject(value, name) {
|
|
41
|
+
if (value === undefined)
|
|
42
|
+
return {};
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw invalid(name, `${name} is not valid JSON.`, `Pass a JSON object, for example {"start":"Start"}.`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
51
|
+
throw invalid(name, `${name} must be a JSON object.`, 'Pass a JSON object.');
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
29
54
|
/** The success document every append answers with. */
|
|
30
55
|
function appended(draft, operation) {
|
|
31
56
|
return 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 }, [{ tool: 'draft_validate', args: { draft_id: draft.id }, why: 'Validate the whole draft before writing anything.' }]);
|
|
@@ -43,7 +68,7 @@ export function draftTools(session, drafts, maxOperations, maxFileBytes) {
|
|
|
43
68
|
return [
|
|
44
69
|
tool({
|
|
45
70
|
name: 'draft_rung',
|
|
46
|
-
description: 'Append one ladder rung operation to the chat draft: replace, insert or delete. Nothing is written until draft_apply.',
|
|
71
|
+
description: 'Append one ladder rung operation to the chat draft: replace, insert or delete. Optionally carries the rung comment. Nothing is written until draft_apply.',
|
|
47
72
|
parameters: object({
|
|
48
73
|
program: string('Program that owns the routine. Example: "MainProgram".'),
|
|
49
74
|
routine: string('Routine the rung belongs to. Example: "MainRoutine".'),
|
|
@@ -51,6 +76,7 @@ export function draftTools(session, drafts, maxOperations, maxFileBytes) {
|
|
|
51
76
|
rung: integer('Rung number the operation targets. Required for replace and delete; for insert it is the new rung number.', { minimum: 0 }),
|
|
52
77
|
text: string('NeutralText for replace or insert. Omit for delete. Example: "XIC(Start)OTE(Run);"'),
|
|
53
78
|
before_rung: integer('For insert only: insert before this rung number instead of after rung.', { minimum: 0 }),
|
|
79
|
+
comment: string('Optional rung comment, written above the rung in Studio. Omit to leave an existing comment unchanged.'),
|
|
54
80
|
project_id: PROJECT_ID,
|
|
55
81
|
}, ['program', 'routine', 'mode']),
|
|
56
82
|
async run(args, exec) {
|
|
@@ -61,6 +87,7 @@ export function draftTools(session, drafts, maxOperations, maxFileBytes) {
|
|
|
61
87
|
const text = optionalString(args, 'text');
|
|
62
88
|
const rung = optionalInteger(args, 'rung');
|
|
63
89
|
const before = optionalInteger(args, 'before_rung');
|
|
90
|
+
const comment = args.comment === undefined ? undefined : String(args.comment);
|
|
64
91
|
let operation;
|
|
65
92
|
if (mode === 'delete') {
|
|
66
93
|
if (rung === undefined)
|
|
@@ -70,12 +97,12 @@ export function draftTools(session, drafts, maxOperations, maxFileBytes) {
|
|
|
70
97
|
else if (mode === 'replace') {
|
|
71
98
|
if (rung === undefined || text === undefined)
|
|
72
99
|
throw invalid('rung and text', 'Required when mode is replace.', 'Pass the rung number and its new NeutralText.');
|
|
73
|
-
operation = { operation: 'replace_rung', program, routine, rung, text };
|
|
100
|
+
operation = { operation: 'replace_rung', program, routine, rung, text, ...(comment === undefined ? {} : { comment }) };
|
|
74
101
|
}
|
|
75
102
|
else if (mode === 'insert') {
|
|
76
103
|
if (rung === undefined || text === undefined)
|
|
77
104
|
throw invalid('rung and text', 'Required when mode is insert.', 'Pass the new rung number and its NeutralText.');
|
|
78
|
-
operation = { operation: 'insert_rung', program, routine, rung, text, ...(before === undefined ? {} : { before_rung: before }) };
|
|
105
|
+
operation = { operation: 'insert_rung', program, routine, rung, text, ...(before === undefined ? {} : { before_rung: before }), ...(comment === undefined ? {} : { comment }) };
|
|
79
106
|
}
|
|
80
107
|
else {
|
|
81
108
|
throw invalid('mode', `Unknown mode ${JSON.stringify(mode)}.`, 'Pass one of: replace, insert, delete.');
|
|
@@ -83,6 +110,85 @@ export function draftTools(session, drafts, maxOperations, maxFileBytes) {
|
|
|
83
110
|
return appended(drafts.append(exec, project.id, operation, maxOperations), operation);
|
|
84
111
|
},
|
|
85
112
|
}),
|
|
113
|
+
tool({
|
|
114
|
+
name: 'draft_rung_comment',
|
|
115
|
+
description: 'Append one rung-comment change to the chat draft. Sets or clears the comment Studio shows above a rung; the logic is untouched. Nothing is written until draft_apply.',
|
|
116
|
+
parameters: object({
|
|
117
|
+
program: string('Program that owns the routine. Example: "MainProgram".'),
|
|
118
|
+
routine: string('Ladder routine the rung belongs to. Example: "MainRoutine".'),
|
|
119
|
+
rung: integer('Rung number whose comment changes.', { minimum: 0 }),
|
|
120
|
+
comment: string('New comment text. Pass an empty string to clear an existing comment.'),
|
|
121
|
+
project_id: PROJECT_ID,
|
|
122
|
+
}, ['program', 'routine', 'rung', 'comment']),
|
|
123
|
+
async run(args, exec) {
|
|
124
|
+
const project = await session.require(optionalString(args, 'project_id'), exec);
|
|
125
|
+
const operation = {
|
|
126
|
+
operation: 'set_rung_comment',
|
|
127
|
+
program: requiredString(args, 'program'),
|
|
128
|
+
routine: requiredString(args, 'routine'),
|
|
129
|
+
rung: optionalInteger(args, 'rung') ?? 0,
|
|
130
|
+
comment: typeof args.comment === 'string' ? args.comment : '',
|
|
131
|
+
};
|
|
132
|
+
return appended(drafts.append(exec, project.id, operation, maxOperations), operation);
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
tool({
|
|
136
|
+
name: 'compose_rung',
|
|
137
|
+
description: 'Compose one validated ladder rung from a structure or a named pattern, checking every instruction and operand against the export, and optionally stage it in the draft. Never emits text that fails the ladder parser.',
|
|
138
|
+
parameters: object({
|
|
139
|
+
pattern: choice('Named pattern to expand. fault_latch produces two rungs.', PATTERNS),
|
|
140
|
+
spec_json: string('Composition tree as JSON, for example {"series":[{"instr":"XIC","args":["Start"]},{"instr":"OTE","args":["Run"]}]}.'),
|
|
141
|
+
arguments_json: string('Pattern arguments as JSON, for example {"start":"Start","stop":"Stop","run":"Run"}.'),
|
|
142
|
+
program: string('Program to stage into. Omit to return the composed text only.'),
|
|
143
|
+
routine: string('Routine to stage into. Required with program.'),
|
|
144
|
+
mode: choice('insert adds a rung; replace rewrites one.', ['insert', 'replace']),
|
|
145
|
+
rung: integer('Rung number for a staged operation. For a two-rung pattern, the second uses rung + 1.', { minimum: 0 }),
|
|
146
|
+
before_rung: integer('For insert: insert before this rung number instead of after rung.', { minimum: 0 }),
|
|
147
|
+
comment: string('Optional rung comment for the staged rung.'),
|
|
148
|
+
draft_id: string('Draft handle to append to. Omit when exactly one draft exists in this chat.'),
|
|
149
|
+
project_id: PROJECT_ID,
|
|
150
|
+
}, []),
|
|
151
|
+
async run(args, exec) {
|
|
152
|
+
const project = await session.require(optionalString(args, 'project_id'), exec);
|
|
153
|
+
const pattern = optionalString(args, 'pattern');
|
|
154
|
+
const specJson = optionalString(args, 'spec_json');
|
|
155
|
+
if ((pattern === undefined) === (specJson === undefined)) {
|
|
156
|
+
throw invalid('pattern or spec_json', 'Pass exactly one of a named pattern or a composition tree.', 'Use pattern + arguments_json for a common shape, or spec_json for explicit logic.');
|
|
157
|
+
}
|
|
158
|
+
let composed;
|
|
159
|
+
if (pattern !== undefined) {
|
|
160
|
+
if (!PATTERNS.includes(pattern))
|
|
161
|
+
throw invalid('pattern', `Unknown pattern ${JSON.stringify(pattern)}.`, `Pass one of: ${PATTERNS.join(', ')}.`);
|
|
162
|
+
composed = composePattern(project, pattern, jsonObject(typeof args.arguments_json === 'string' ? args.arguments_json : undefined, 'arguments_json'));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const spec = jsonObject(specJson, 'spec_json');
|
|
166
|
+
composed = { rungs: [composeRung(project, spec)] };
|
|
167
|
+
}
|
|
168
|
+
const program = optionalString(args, 'program');
|
|
169
|
+
const routine = optionalString(args, 'routine');
|
|
170
|
+
const mode = optionalString(args, 'mode');
|
|
171
|
+
const rung = optionalInteger(args, 'rung');
|
|
172
|
+
const staged = program !== undefined || routine !== undefined || mode !== undefined || rung !== undefined;
|
|
173
|
+
if (!staged) {
|
|
174
|
+
return success(`Composed ${composed.rungs.length} rung(s)${composed.pattern === undefined ? '' : ` from ${composed.pattern}`}; pass program and routine to stage it.`, { pattern: composed.pattern, rungs: composed.rungs }, [{ tool: 'compose_rung', args: { spec_json: '{"series":[]}' }, why: 'Adjust the structure, or stage this text with draft_rung.' }], COMPOSED_COVERAGE);
|
|
175
|
+
}
|
|
176
|
+
if (program === undefined || routine === undefined || mode === undefined || rung === undefined) {
|
|
177
|
+
throw invalid('program, routine, mode and rung', 'Staging needs all four.', 'Pass program, routine, mode and rung together, or none of them.');
|
|
178
|
+
}
|
|
179
|
+
if (composed.rungs.length > 1 && mode === 'replace')
|
|
180
|
+
throw invalid('mode', 'A two-rung pattern cannot replace one rung.', 'Use mode insert, or compose a single rung.');
|
|
181
|
+
let draft;
|
|
182
|
+
for (const [index, composedRung] of composed.rungs.entries()) {
|
|
183
|
+
const operation = mode === 'replace'
|
|
184
|
+
? { operation: 'replace_rung', program, routine, rung, text: composedRung.neutral_text, ...(typeof args.comment === 'string' ? { comment: args.comment } : {}) }
|
|
185
|
+
: { operation: 'insert_rung', program, routine, rung: rung + index, text: composedRung.neutral_text, ...(index === 0 && args.before_rung !== undefined ? { before_rung: optionalInteger(args, 'before_rung') } : {}), ...(typeof args.comment === 'string' ? { comment: args.comment } : {}) };
|
|
186
|
+
draft = drafts.append(exec, project.id, operation, maxOperations);
|
|
187
|
+
}
|
|
188
|
+
const stagedDraft = draft;
|
|
189
|
+
return success(`Composed and staged ${composed.rungs.length} rung(s)${composed.pattern === undefined ? '' : ` from ${composed.pattern}`}. Draft ${stagedDraft.id}: ${stagedDraft.operations.length} operation(s).`, { draft_id: stagedDraft.id, operation_count: stagedDraft.operations.length, pattern: composed.pattern, rungs: composed.rungs }, [{ tool: 'draft_validate', args: { draft_id: stagedDraft.id }, why: 'Validate the whole draft before writing anything.' }], COMPOSED_COVERAGE);
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
86
192
|
tool({
|
|
87
193
|
name: 'draft_st',
|
|
88
194
|
description: 'Append one Structured Text body replacement to the chat draft. Nothing is written until draft_apply.',
|