@aryacode/dsh-engineering 0.5.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 +52 -0
- package/README.md +9 -8
- package/cordis.patch.yml +1 -1
- package/entries/netlogic.js +266 -0
- package/entries/optix.js +13 -0
- package/package.json +10 -10
- 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
- package/vendor/optix/SKILL.md +68 -0
- package/vendor/optix/netlogic/SOURCE.json +12 -0
- package/vendor/optix/netlogic/index.json +1987 -0
- package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
- package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
- package/vendor/optix/netlogic/pages/alarming.md +489 -0
- package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
- package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
- package/vendor/optix/netlogic/pages/colors.md +122 -0
- package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
- package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
- package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
- package/vendor/optix/netlogic/pages/datagrids.md +123 -0
- package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
- package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
- package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
- package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
- package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
- package/vendor/optix/netlogic/pages/element-access.md +1208 -0
- package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
- package/vendor/optix/netlogic/pages/events.md +340 -0
- package/vendor/optix/netlogic/pages/execute-command.md +202 -0
- package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
- package/vendor/optix/netlogic/pages/good-practices.md +50 -0
- package/vendor/optix/netlogic/pages/information-model.md +89 -0
- package/vendor/optix/netlogic/pages/log-output.md +22 -0
- package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
- package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
- package/vendor/optix/netlogic/pages/mqtt.md +240 -0
- package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
- package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
- package/vendor/optix/netlogic/pages/opcua.md +325 -0
- package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
- package/vendor/optix/netlogic/pages/queries.md +1297 -0
- package/vendor/optix/netlogic/pages/random-generation.md +90 -0
- package/vendor/optix/netlogic/pages/recipes.md +375 -0
- package/vendor/optix/netlogic/pages/recipex.md +2383 -0
- package/vendor/optix/netlogic/pages/regex.md +15 -0
- package/vendor/optix/netlogic/pages/register-observers.md +346 -0
- package/vendor/optix/netlogic/pages/reports.md +141 -0
- package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
- package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
- package/vendor/optix/netlogic/pages/serial-port.md +357 -0
- package/vendor/optix/netlogic/pages/sessions.md +58 -0
- package/vendor/optix/netlogic/pages/system-node.md +153 -0
- package/vendor/optix/netlogic/pages/template-library.md +127 -0
- package/vendor/optix/netlogic/pages/threading.md +236 -0
- package/vendor/optix/netlogic/pages/translations.md +417 -0
- package/vendor/optix/netlogic/pages/trends.md +128 -0
- package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
- package/vendor/optix/netlogic/pages/users-groups.md +416 -0
- package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
- package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
- package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose one ladder rung from a structure, never from prose.
|
|
3
|
+
*
|
|
4
|
+
* The composer is the generation half of the pair: the caller decides the logic,
|
|
5
|
+
* this module decides the NeutralText. Every instruction is checked against the
|
|
6
|
+
* catalog (or a declared AOI), every operand against the export, and the emitted
|
|
7
|
+
* text is re-parsed by the same parser `draft_rung` uses. A rung that fails any
|
|
8
|
+
* of those never reaches a draft.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-engineering-l5x/engine/ladder
|
|
11
|
+
*/
|
|
12
|
+
import { L5xError } from "../core/errors.js";
|
|
13
|
+
import { resolveTag } from "./tag-resolution.js";
|
|
14
|
+
import { instructionByName, nearestInstructions, NUMERIC_TYPES, TIMER_TYPES } from "./instructions.js";
|
|
15
|
+
import { parseLadder } from "./logic-syntax.js";
|
|
16
|
+
/** Named patterns the composer can expand. */
|
|
17
|
+
export const PATTERNS = ['start_stop_latch', 'fault_latch', 'ton_delay', 'interlock'];
|
|
18
|
+
const MAX_INSTRUCTIONS = 64;
|
|
19
|
+
const MAX_DEPTH = 16;
|
|
20
|
+
const LITERAL = /^-?(?:\d+\.?\d*|\.\d+)$/u;
|
|
21
|
+
const TAG_TEXT = /^[A-Za-z_][A-Za-z0-9_:.[\]]*$/u;
|
|
22
|
+
/** A numeric literal, which is not a declaration. */
|
|
23
|
+
const isLiteral = (text) => LITERAL.test(text);
|
|
24
|
+
/** A BOOL-ish check for the slot validation below uses this resolved operand. */
|
|
25
|
+
function resolveOperand(document, text) {
|
|
26
|
+
const direct = resolveTag(document, 'controller', text);
|
|
27
|
+
if (direct.resolved)
|
|
28
|
+
return direct;
|
|
29
|
+
// A rung is written inside a program, so an unqualified program tag is the
|
|
30
|
+
// ordinary case. Resolve it when exactly one program declares the base name;
|
|
31
|
+
// two programs declaring it is an ambiguity the caller must settle.
|
|
32
|
+
const base = text.split(/[.[:]/u)[0] ?? text;
|
|
33
|
+
const declared = document.tags.filter(tag => tag.name.toUpperCase() === base.toUpperCase());
|
|
34
|
+
if (declared.length === 1)
|
|
35
|
+
return resolveTag(document, declared[0].scope, text);
|
|
36
|
+
if (declared.length > 1) {
|
|
37
|
+
throw new L5xError('AMBIGUOUS_TAG', `${base} is declared in ${declared.length} scopes.`, 'Declare the tag once, or stage the rung through a routine in the program that owns it.', declared.map(tag => tag.scope));
|
|
38
|
+
}
|
|
39
|
+
return direct;
|
|
40
|
+
}
|
|
41
|
+
/** One tag or literal operand, checked against its slot kind. */
|
|
42
|
+
function checkOperand(project, text, kind, seen) {
|
|
43
|
+
if (isLiteral(text)) {
|
|
44
|
+
// A literal is a value, not an address: only the slots that take a value accept it.
|
|
45
|
+
if (kind === 'BOOL')
|
|
46
|
+
throw new L5xError('INVALID_OPERAND', `${text} is a number where a BOOL tag is required.`, 'Pass a declared BOOL tag.');
|
|
47
|
+
if (kind === 'TIMER')
|
|
48
|
+
throw new L5xError('INVALID_OPERAND', `${text} is a number where a timer is required.`, 'Pass a declared TIMER tag.');
|
|
49
|
+
const operand = { text, resolved: false };
|
|
50
|
+
seen.push(operand);
|
|
51
|
+
return operand;
|
|
52
|
+
}
|
|
53
|
+
if (!TAG_TEXT.test(text)) {
|
|
54
|
+
throw new L5xError('INVALID_OPERAND', `"${text}" is not a tag or a number.`, 'Pass a declared tag name, a member path, or a numeric literal; no spaces or quotes.');
|
|
55
|
+
}
|
|
56
|
+
const resolution = resolveOperand(project.document, text);
|
|
57
|
+
if (!resolution.resolved) {
|
|
58
|
+
const near = project.document.tags
|
|
59
|
+
.map(tag => tag.name)
|
|
60
|
+
.filter(name => name.toLowerCase().includes(text.split('.')[0].toLowerCase()))
|
|
61
|
+
.slice(0, 5);
|
|
62
|
+
throw new L5xError('TAG_NOT_FOUND', `No tag named "${text}" resolves in this export.`, near.length === 0 ? 'Call list_components kind="tag" to see the declared names.' : 'Retry with one of the candidates, or declare the tag first.', near);
|
|
63
|
+
}
|
|
64
|
+
const type = (resolution.data_type ?? '').toUpperCase();
|
|
65
|
+
const operand = { text, resolved: true, canonical: resolution.canonical ?? text, data_type: type === '' ? undefined : type };
|
|
66
|
+
seen.push(operand);
|
|
67
|
+
if (kind === 'BOOL' && type !== 'BOOL') {
|
|
68
|
+
throw new L5xError('INVALID_OPERAND', `${text} is ${type || 'an unknown type'}; this slot needs a BOOL tag.`, 'Use a BOOL tag, or an alias that resolves to BOOL.', []);
|
|
69
|
+
}
|
|
70
|
+
if (kind === 'NUMBER' && !NUMERIC_TYPES.has(type)) {
|
|
71
|
+
throw new L5xError('INVALID_OPERAND', `${text} is ${type || 'an unknown type'}; this slot needs a numeric value or tag.`, 'Pass a numeric literal or an INT, DINT, REAL or similar tag.', []);
|
|
72
|
+
}
|
|
73
|
+
if (kind === 'TIMER' && !TIMER_TYPES.has(type)) {
|
|
74
|
+
throw new L5xError('INVALID_OPERAND', `${text} is ${type || 'an unknown type'}; this slot needs a timer.`, 'Pass a declared TIMER tag (TON, TOF or RTO instance).', []);
|
|
75
|
+
}
|
|
76
|
+
return operand;
|
|
77
|
+
}
|
|
78
|
+
/** Resolve one instruction node and emit it. */
|
|
79
|
+
function composeInstruction(project, node, operands, counter) {
|
|
80
|
+
const args = node.args ?? [];
|
|
81
|
+
counter.instructions += 1;
|
|
82
|
+
if (counter.instructions > MAX_INSTRUCTIONS) {
|
|
83
|
+
throw new L5xError('LADDER_TOO_LARGE', `One rung may hold at most ${MAX_INSTRUCTIONS} instructions.`, 'Split the logic across rungs.');
|
|
84
|
+
}
|
|
85
|
+
const instruction = instructionByName(node.instr);
|
|
86
|
+
if (instruction !== undefined) {
|
|
87
|
+
const required = instruction.operands.filter(slot => slot.optional !== true).length;
|
|
88
|
+
if (args.length < required || args.length > instruction.operands.length) {
|
|
89
|
+
const shape = instruction.operands.map(slot => `${slot.name}${slot.optional === true ? '?' : ''}`).join(', ');
|
|
90
|
+
throw new L5xError('INVALID_OPERAND', `${instruction.name} takes ${required === instruction.operands.length ? String(required) : `${required}-${instruction.operands.length}`} operand(s): ${shape}; ${args.length} were supplied.`, `Call list_instructions with filter "${instruction.name}" for the exact operand order.`);
|
|
91
|
+
}
|
|
92
|
+
for (const [index, argument] of args.entries())
|
|
93
|
+
checkOperand(project, argument, instruction.operands[index].kind, operands);
|
|
94
|
+
return { text: `${instruction.name}(${args.join(',')})`, grounding: 'catalog' };
|
|
95
|
+
}
|
|
96
|
+
const aoi = project.document.aois.find(entry => entry.name.toUpperCase() === node.instr.toUpperCase());
|
|
97
|
+
if (aoi === undefined) {
|
|
98
|
+
throw new L5xError('UNKNOWN_INSTRUCTION', `"${node.instr}" is neither a catalogued instruction nor an AOI declared in this export.`, 'Call list_instructions for the catalog; an AOI must be declared in the project.', nearestInstructions(node.instr));
|
|
99
|
+
}
|
|
100
|
+
// AOI pins are declared by the AOI, not by this catalog; the operand text is
|
|
101
|
+
// still resolved so a typo in a tag cannot reach the draft.
|
|
102
|
+
for (const argument of args)
|
|
103
|
+
checkOperand(project, argument, 'ANY', operands);
|
|
104
|
+
return { text: `${aoi.name}(${args.join(',')})`, grounding: 'aoi' };
|
|
105
|
+
}
|
|
106
|
+
/** Emit one node tree, validating as it goes. */
|
|
107
|
+
function composeNode(project, node, operands, counter, depth, grounds) {
|
|
108
|
+
if (depth > MAX_DEPTH)
|
|
109
|
+
throw new L5xError('LADDER_TOO_LARGE', `Branch nesting exceeds ${MAX_DEPTH}.`, 'Flatten the branches.');
|
|
110
|
+
if ('parallel' in node) {
|
|
111
|
+
if (!Array.isArray(node.parallel) || node.parallel.length < 2) {
|
|
112
|
+
throw new L5xError('INVALID_LADDER_SPEC', 'A parallel branch needs at least two paths.', 'Wrap the alternative paths in "parallel": [[...],[...]].');
|
|
113
|
+
}
|
|
114
|
+
const paths = node.parallel.map(path => {
|
|
115
|
+
if (!Array.isArray(path) || path.length === 0)
|
|
116
|
+
throw new L5xError('INVALID_LADDER_SPEC', 'A parallel path cannot be empty.', 'Give every path at least one instruction.');
|
|
117
|
+
return path.map(entry => composeNode(project, entry, operands, counter, depth + 1, grounds)).join('');
|
|
118
|
+
});
|
|
119
|
+
return `[${paths.join(' ,')} ]`;
|
|
120
|
+
}
|
|
121
|
+
if ('series' in node) {
|
|
122
|
+
if (!Array.isArray(node.series) || node.series.length === 0) {
|
|
123
|
+
throw new L5xError('INVALID_LADDER_SPEC', 'A series cannot be empty.', 'Give the series at least one instruction.');
|
|
124
|
+
}
|
|
125
|
+
return node.series.map(entry => composeNode(project, entry, operands, counter, depth + 1, grounds)).join('');
|
|
126
|
+
}
|
|
127
|
+
const composed = composeInstruction(project, node, operands, counter);
|
|
128
|
+
grounds.add(composed.grounding);
|
|
129
|
+
return composed.text;
|
|
130
|
+
}
|
|
131
|
+
/** Validate the rung we just emitted with the parser the draft path uses. */
|
|
132
|
+
function verify(project, text, operands, instructions, grounds) {
|
|
133
|
+
const parsed = parseLadder(text);
|
|
134
|
+
if (parsed.issues.length > 0) {
|
|
135
|
+
// The composer produced text its own parser rejects: a defect, never a draft.
|
|
136
|
+
throw new L5xError('COMPOSER_DEFECT', `The composed rung does not parse: ${parsed.issues[0]?.message ?? 'unknown issue'}`, 'This is a plugin defect; report the spec that produced it.');
|
|
137
|
+
}
|
|
138
|
+
const grounding = grounds.size === 2 ? 'mixed' : grounds.has('aoi') ? 'aoi' : 'catalog';
|
|
139
|
+
return { neutral_text: text, instructions, operands, grounding };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Compose one rung from a structure tree.
|
|
143
|
+
*
|
|
144
|
+
* @param project - Fresh export snapshot the operands are resolved against.
|
|
145
|
+
* @param spec - The composition tree.
|
|
146
|
+
* @returns The validated rung.
|
|
147
|
+
*/
|
|
148
|
+
export function composeRung(project, spec) {
|
|
149
|
+
const operands = [];
|
|
150
|
+
const counter = { instructions: 0 };
|
|
151
|
+
const grounds = new Set();
|
|
152
|
+
const text = `${composeNode(project, spec, operands, counter, 0, grounds)};`;
|
|
153
|
+
if (counter.instructions === 0)
|
|
154
|
+
throw new L5xError('INVALID_LADDER_SPEC', 'The specification contains no instruction.', 'Give the rung at least one instruction.');
|
|
155
|
+
return verify(project, text, operands, counter.instructions, grounds);
|
|
156
|
+
}
|
|
157
|
+
/** A required pattern argument. */
|
|
158
|
+
function arg(args, name) {
|
|
159
|
+
const value = args[name];
|
|
160
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
161
|
+
throw new L5xError('INVALID_ARGUMENT', `Pattern argument ${name} must be a non-empty string.`, `Pass ${name} as a declared tag name.`);
|
|
162
|
+
}
|
|
163
|
+
return value.trim();
|
|
164
|
+
}
|
|
165
|
+
/** A pattern's named tags, expanded to one or two rungs. */
|
|
166
|
+
export function composePattern(project, pattern, args) {
|
|
167
|
+
const one = (spec) => ({ rungs: [composeRung(project, spec)], pattern });
|
|
168
|
+
if (pattern === 'start_stop_latch') {
|
|
169
|
+
const start = arg(args, 'start');
|
|
170
|
+
const stop = arg(args, 'stop');
|
|
171
|
+
const run = arg(args, 'run');
|
|
172
|
+
return one({ series: [
|
|
173
|
+
{ parallel: [[{ instr: 'XIC', args: [start] }], [{ instr: 'XIC', args: [run] }]] },
|
|
174
|
+
{ instr: 'XIO', args: [stop] },
|
|
175
|
+
{ instr: 'OTE', args: [run] },
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
if (pattern === 'interlock') {
|
|
179
|
+
const conditions = args.conditions;
|
|
180
|
+
if (!Array.isArray(conditions) || conditions.length === 0) {
|
|
181
|
+
throw new L5xError('INVALID_ARGUMENT', 'interlock needs conditions: at least one tag.', 'Pass conditions as an array of declared BOOL tag names.');
|
|
182
|
+
}
|
|
183
|
+
const output = arg(args, 'output');
|
|
184
|
+
return one({ series: [
|
|
185
|
+
...conditions.map(condition => ({ instr: 'XIC', args: [String(condition)] })),
|
|
186
|
+
{ instr: 'OTE', args: [output] },
|
|
187
|
+
] });
|
|
188
|
+
}
|
|
189
|
+
if (pattern === 'ton_delay') {
|
|
190
|
+
const enable = arg(args, 'enable');
|
|
191
|
+
const timer = arg(args, 'timer');
|
|
192
|
+
const preset = args.preset_ms;
|
|
193
|
+
if (typeof preset !== 'number' || !Number.isFinite(preset) || preset < 0) {
|
|
194
|
+
throw new L5xError('INVALID_ARGUMENT', 'ton_delay needs preset_ms as a non-negative number.', 'Pass the preset in milliseconds, for example 1500.');
|
|
195
|
+
}
|
|
196
|
+
return one({ series: [{ instr: 'XIC', args: [enable] }, { instr: 'TON', args: [timer, String(preset)] }] });
|
|
197
|
+
}
|
|
198
|
+
// fault_latch: set on the fault, clear on the reset — two rungs, never one.
|
|
199
|
+
const fault = arg(args, 'fault');
|
|
200
|
+
const reset = arg(args, 'reset');
|
|
201
|
+
const latched = arg(args, 'latched');
|
|
202
|
+
return {
|
|
203
|
+
pattern,
|
|
204
|
+
rungs: [
|
|
205
|
+
composeRung(project, { series: [{ instr: 'XIC', args: [fault] }, { instr: 'OTL', args: [latched] }] }),
|
|
206
|
+
composeRung(project, { series: [{ instr: 'XIC', args: [reset] }, { instr: 'OTU', args: [latched] }] }),
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -31,6 +31,7 @@ export { ProjectSession } from './session.ts';
|
|
|
31
31
|
export { readTools } from './tools/read.ts';
|
|
32
32
|
export { referenceTools } from './tools/references.ts';
|
|
33
33
|
export { checkTools } from './tools/checks.ts';
|
|
34
|
+
export { documentTools } from './tools/document.ts';
|
|
34
35
|
export { L5xError } from './core/errors.ts';
|
|
35
36
|
export type { NextStep, Coverage, Success } from './core/result.ts';
|
|
36
37
|
export type { ToolDefinition } from './core/tool.ts';
|
package/vendor/l5x/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ProjectSession } from "./session.js";
|
|
|
12
12
|
import { readTools } from "./tools/read.js";
|
|
13
13
|
import { referenceTools } from "./tools/references.js";
|
|
14
14
|
import { checkTools } from "./tools/checks.js";
|
|
15
|
+
import { documentTools } from "./tools/document.js";
|
|
15
16
|
/** Plugin name in a composition. */
|
|
16
17
|
export const name = 'l5x-read';
|
|
17
18
|
/** Services this plugin cannot register without. */
|
|
@@ -31,7 +32,7 @@ export function apply(ctx, config = {}) {
|
|
|
31
32
|
}
|
|
32
33
|
const session = new ProjectSession(ctx.fs, maxFileBytes);
|
|
33
34
|
ctx.provide?.(PROJECTS_SERVICE, session);
|
|
34
|
-
for (const definition of [...readTools(session), ...referenceTools(session), ...checkTools(session)]) {
|
|
35
|
+
for (const definition of [...readTools(session), ...referenceTools(session), ...checkTools(session), ...documentTools(session)]) {
|
|
35
36
|
ctx.tools.register(definition);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -39,4 +40,5 @@ export { ProjectSession } from "./session.js";
|
|
|
39
40
|
export { readTools } from "./tools/read.js";
|
|
40
41
|
export { referenceTools } from "./tools/references.js";
|
|
41
42
|
export { checkTools } from "./tools/checks.js";
|
|
43
|
+
export { documentTools } from "./tools/document.js";
|
|
42
44
|
export { L5xError } from "./core/errors.js";
|
|
@@ -175,7 +175,12 @@ function decodeLines(routine) {
|
|
|
175
175
|
function decodeRung(rung) {
|
|
176
176
|
const textElement = firstChild(rung, 'Text');
|
|
177
177
|
const text = textElement === undefined ? '' : (nodeText(textElement) ?? '').trim();
|
|
178
|
-
|
|
178
|
+
// Current exports write the comment as a <Comment> child (CDATA); older ones
|
|
179
|
+
// used the attribute. Reading only the attribute dropped 572 real comments in
|
|
180
|
+
// one project, which is what a documentation pass needs most.
|
|
181
|
+
const commentElement = firstChild(rung, 'Comment');
|
|
182
|
+
const fromChild = commentElement === undefined ? undefined : nodeText(commentElement);
|
|
183
|
+
const comment = (fromChild ?? optionalAttribute(rung, 'Comment', '')).trim();
|
|
179
184
|
const number = Number.parseInt(attribute(rung, 'Number'), 10);
|
|
180
185
|
if (comment === '')
|
|
181
186
|
return { number, text };
|
|
@@ -101,6 +101,26 @@ export declare class ProjectSession {
|
|
|
101
101
|
* @returns The resolved path.
|
|
102
102
|
*/
|
|
103
103
|
resolvePath(requestedPath: string, exec?: ExecContext): Promise<string>;
|
|
104
|
+
/**
|
|
105
|
+
* Whether one path already exists.
|
|
106
|
+
*
|
|
107
|
+
* @param requestedPath - Path as the caller supplied it.
|
|
108
|
+
* @param exec - Calling context: cancellation and workspace root.
|
|
109
|
+
* @returns True when the provider reports a file or folder there.
|
|
110
|
+
*/
|
|
111
|
+
exists(requestedPath: string, exec?: ExecContext): Promise<boolean>;
|
|
112
|
+
/**
|
|
113
|
+
* Write one text file at an exact path, without the `.L5X` restriction.
|
|
114
|
+
*
|
|
115
|
+
* Used by the documentation bundle: the provider creates missing parent
|
|
116
|
+
* folders, so a new `output_root` needs no separate mkdir.
|
|
117
|
+
*
|
|
118
|
+
* @param requestedPath - Destination as the caller supplied it.
|
|
119
|
+
* @param text - File contents.
|
|
120
|
+
* @param exec - Calling context: cancellation and workspace root.
|
|
121
|
+
* @returns The resolved destination.
|
|
122
|
+
*/
|
|
123
|
+
writeTextAt(requestedPath: string, text: string, exec?: ExecContext): Promise<string>;
|
|
104
124
|
/**
|
|
105
125
|
* Write one prepared draft to a new path, never overwriting.
|
|
106
126
|
*
|
|
@@ -144,6 +144,42 @@ export class ProjectSession {
|
|
|
144
144
|
});
|
|
145
145
|
return target.displayPath;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether one path already exists.
|
|
149
|
+
*
|
|
150
|
+
* @param requestedPath - Path as the caller supplied it.
|
|
151
|
+
* @param exec - Calling context: cancellation and workspace root.
|
|
152
|
+
* @returns True when the provider reports a file or folder there.
|
|
153
|
+
*/
|
|
154
|
+
async exists(requestedPath, exec) {
|
|
155
|
+
const target = await this.fs.resolve(requestedPath, {
|
|
156
|
+
...(exec?.cwd === undefined ? {} : { cwd: exec.cwd }),
|
|
157
|
+
...(exec?.signal === undefined ? {} : { signal: exec.signal }),
|
|
158
|
+
});
|
|
159
|
+
return (await this.fs.stat(target, exec?.signal)) !== undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Write one text file at an exact path, without the `.L5X` restriction.
|
|
163
|
+
*
|
|
164
|
+
* Used by the documentation bundle: the provider creates missing parent
|
|
165
|
+
* folders, so a new `output_root` needs no separate mkdir.
|
|
166
|
+
*
|
|
167
|
+
* @param requestedPath - Destination as the caller supplied it.
|
|
168
|
+
* @param text - File contents.
|
|
169
|
+
* @param exec - Calling context: cancellation and workspace root.
|
|
170
|
+
* @returns The resolved destination.
|
|
171
|
+
*/
|
|
172
|
+
async writeTextAt(requestedPath, text, exec) {
|
|
173
|
+
if (this.fs.writeText === undefined) {
|
|
174
|
+
throw new L5xError('OUTPUT_UNAVAILABLE', 'The filesystem provider in this profile cannot write files.', 'Use a profile whose filesystem provider allows writes.');
|
|
175
|
+
}
|
|
176
|
+
const target = await this.fs.resolve(requestedPath, {
|
|
177
|
+
...(exec?.cwd === undefined ? {} : { cwd: exec.cwd }),
|
|
178
|
+
...(exec?.signal === undefined ? {} : { signal: exec.signal }),
|
|
179
|
+
});
|
|
180
|
+
await this.fs.writeText(target, text, { kind: 'createIfAbsent' }, exec?.signal);
|
|
181
|
+
return target.displayPath;
|
|
182
|
+
}
|
|
147
183
|
/**
|
|
148
184
|
* Write one prepared draft to a new path, never overwriting.
|
|
149
185
|
*
|
|
@@ -0,0 +1,18 @@
|
|
|
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 type { ToolDefinition } from '../core/tool.ts';
|
|
11
|
+
import type { ProjectSession } from '../session.ts';
|
|
12
|
+
/**
|
|
13
|
+
* The documentation tools.
|
|
14
|
+
*
|
|
15
|
+
* @param session - Project handles for the calling chat.
|
|
16
|
+
* @returns Tool definitions in registration order.
|
|
17
|
+
*/
|
|
18
|
+
export declare function documentTools(session: ProjectSession): readonly ToolDefinition[];
|
|
@@ -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.',
|