@manny-est/node-red-flowpilot 0.5.0 → 0.5.2
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 +94 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +22 -13
- package/flowpilot-core.css +187 -2
- package/flowpilot.js +462 -33
- package/lib/build-system-prompt.js +26 -4
- package/lib/core/apply-review.js +494 -64
- package/lib/core/init.js +67 -132
- package/lib/core/main.js +160 -7
- package/lib/core/modes.js +876 -79
- package/lib/core/selection-context.js +28 -1
- package/lib/default-system-prompt.js +13 -9
- package/lib/document-system-prompt.js +19 -30
- package/lib/generation-system-prompt.js +24 -40
- package/lib/modify-system-prompt.js +98 -70
- package/lib/prompt-fragments.js +45 -0
- package/lib/provider-anthropic.js +385 -0
- package/lib/provider-openai-compatible.js +122 -16
- package/lib/storage.js +43 -2
- package/lib/validator.js +238 -0
- package/package.json +2 -2
package/lib/validator.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// W2 — Class A validator / repair layer.
|
|
4
|
+
//
|
|
5
|
+
// Rules that are mechanically checkable or repairable belong here, not in
|
|
6
|
+
// the model's attention budget. For each repair implemented, the
|
|
7
|
+
// corresponding prompt rule is deleted or shortened in the same commit —
|
|
8
|
+
// the win is measured in prompt shrinkage + corpus improvement, not in
|
|
9
|
+
// code added.
|
|
10
|
+
//
|
|
11
|
+
// Server-side mirror of the client-side DIFF_SKIP in apply-review.js.
|
|
12
|
+
// When a field appears here, it is stripped from changes[].set before
|
|
13
|
+
// the diff reaches the client (rule A6). Keep this list in sync with
|
|
14
|
+
// DIFF_SKIP in apply-review.js.
|
|
15
|
+
const DIFF_SKIP_SERVER = {
|
|
16
|
+
// Appearance-tab metadata (all node types)
|
|
17
|
+
info: 1, inputLabels: 1, outputLabels: 1, icon: 1,
|
|
18
|
+
// debug node display flags
|
|
19
|
+
console: 1, tostatus: 1, targetType: 1, statusVal: 1, statusType: 1,
|
|
20
|
+
// function node internal flags
|
|
21
|
+
noerr: 1, initialize: 1, finalize: 1,
|
|
22
|
+
// mqtt in/out retain handling
|
|
23
|
+
rh: 1
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// A1: missing wires on a non-comment node → inject [].
|
|
27
|
+
// A2: comment node with non-empty wires → force empty.
|
|
28
|
+
// A3: x/y/z present on a node → strip (editor handles placement).
|
|
29
|
+
// A4: tab/subflow type → remove from array entirely.
|
|
30
|
+
function repairFlowNodes(nodes, repairs) {
|
|
31
|
+
if (!Array.isArray(nodes)) { return nodes; }
|
|
32
|
+
const out = [];
|
|
33
|
+
nodes.forEach(function (n) {
|
|
34
|
+
if (!n || typeof n !== "object") { return; }
|
|
35
|
+
|
|
36
|
+
// A4: reject tab/subflow — editor types, not importable nodes
|
|
37
|
+
if (n.type === "tab" || n.type === "subflow") {
|
|
38
|
+
repairs.push({ rule: "A4", detail: "removed " + n.type + " node id=" + (n.id || "?") });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fixed = Object.assign({}, n);
|
|
43
|
+
|
|
44
|
+
// A3: strip x/y/z (editor assigns positions on import; if present
|
|
45
|
+
// they cause nodes to pile up at exact coordinates instead of
|
|
46
|
+
// being auto-arranged)
|
|
47
|
+
const stripped = [];
|
|
48
|
+
["x", "y", "z"].forEach(function (k) {
|
|
49
|
+
if (k in fixed) { delete fixed[k]; stripped.push(k); }
|
|
50
|
+
});
|
|
51
|
+
if (stripped.length) {
|
|
52
|
+
repairs.push({ rule: "A3", detail: "stripped " + stripped.join(",") + " from id=" + (n.id || "?") });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (n.type === "comment") {
|
|
56
|
+
// A2: comment nodes are passive annotations — wires must be empty
|
|
57
|
+
if (Array.isArray(n.wires) && n.wires.some(function (p) { return Array.isArray(p) && p.length > 0; })) {
|
|
58
|
+
fixed.wires = [];
|
|
59
|
+
repairs.push({ rule: "A2", detail: "forced wires:[] on comment id=" + (n.id || "?") });
|
|
60
|
+
} else if (!Array.isArray(n.wires)) {
|
|
61
|
+
fixed.wires = [];
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// A1: every non-comment node must have a wires array
|
|
65
|
+
if (!Array.isArray(n.wires)) {
|
|
66
|
+
fixed.wires = [];
|
|
67
|
+
repairs.push({ rule: "A1", detail: "injected wires:[] on id=" + (n.id || "?") + " type=" + (n.type || "?") });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
out.push(fixed);
|
|
72
|
+
});
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A5: http-request node headers in {key, value} shape → transform to
|
|
77
|
+
// {keyType, keyValue, valueType, valueValue}. The flat shape is silently
|
|
78
|
+
// ignored by Node-RED; the editor uses the keyed shape exclusively.
|
|
79
|
+
function repairHttpHeaders(headers) {
|
|
80
|
+
if (!Array.isArray(headers)) { return headers; }
|
|
81
|
+
return headers.map(function (h) {
|
|
82
|
+
if (h && typeof h === "object" &&
|
|
83
|
+
"key" in h && "value" in h &&
|
|
84
|
+
!("keyType" in h)) {
|
|
85
|
+
return {
|
|
86
|
+
keyType: "other", keyValue: String(h.key || ""),
|
|
87
|
+
valueType: "other", valueValue: String(h.value || "")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return h;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A6: forbidden fields in changes[].set → strip (server-side DIFF_SKIP).
|
|
95
|
+
// A7: group inside set → strip (group membership changes via newGroups only).
|
|
96
|
+
// A8: http-request headers {key,value} shape → transform.
|
|
97
|
+
// A9: same id in changes AND removeNodes → drop from changes.
|
|
98
|
+
//
|
|
99
|
+
// Note: redaction-placeholder stripping (formerly A8 in planning notes)
|
|
100
|
+
// was implemented as W0.2 (stripRedactionPlaceholders in flowpilot.js)
|
|
101
|
+
// and runs after this function — no duplication needed here.
|
|
102
|
+
function repairChanges(changes, removeNodes, repairs) {
|
|
103
|
+
if (!Array.isArray(changes)) { return changes; }
|
|
104
|
+
const removeSet = new Set(Array.isArray(removeNodes) ? removeNodes : []);
|
|
105
|
+
|
|
106
|
+
return changes.filter(function (entry) {
|
|
107
|
+
if (!entry || typeof entry !== "object") { return false; }
|
|
108
|
+
// A9: id appears in both changes and removeNodes — removeNodes wins
|
|
109
|
+
if (entry.id && removeSet.has(entry.id)) {
|
|
110
|
+
repairs.push({ rule: "A9", detail: "dropped id=" + entry.id + " from changes (also in removeNodes)" });
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}).map(function (entry) {
|
|
115
|
+
if (!entry.set || typeof entry.set !== "object") { return entry; }
|
|
116
|
+
const cleanSet = {};
|
|
117
|
+
const droppedA6 = [];
|
|
118
|
+
let droppedGroup = false;
|
|
119
|
+
|
|
120
|
+
Object.keys(entry.set).forEach(function (k) {
|
|
121
|
+
// A6: forbidden internal fields
|
|
122
|
+
if (DIFF_SKIP_SERVER[k]) { droppedA6.push(k); return; }
|
|
123
|
+
// A7: group is informational context, not settable via changes
|
|
124
|
+
if (k === "group") { droppedGroup = true; return; }
|
|
125
|
+
|
|
126
|
+
let v = entry.set[k];
|
|
127
|
+
// A5: http-request node headers on the modify path
|
|
128
|
+
if (k === "headers") { v = repairHttpHeaders(v); }
|
|
129
|
+
|
|
130
|
+
cleanSet[k] = v;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (droppedA6.length) {
|
|
134
|
+
repairs.push({ rule: "A6", detail: "stripped " + droppedA6.join(",") + " from set on id=" + entry.id });
|
|
135
|
+
}
|
|
136
|
+
if (droppedGroup) {
|
|
137
|
+
repairs.push({ rule: "A7", detail: "stripped group from set on id=" + entry.id });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return Object.assign({}, entry, { set: cleanSet });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A10: newWires entries using fromId/toId → rename to from/to. The Modify
|
|
145
|
+
// finalizer consumes only from/to, so leaving the common model-produced
|
|
146
|
+
// aliases in place would make both endpoint references appear missing.
|
|
147
|
+
function repairNewWires(newWires, repairs) {
|
|
148
|
+
if (!Array.isArray(newWires)) { return newWires; }
|
|
149
|
+
return newWires.map(function (wire, index) {
|
|
150
|
+
if (!wire || typeof wire !== "object") { return wire; }
|
|
151
|
+
const fixed = Object.assign({}, wire);
|
|
152
|
+
const renamed = [];
|
|
153
|
+
if (!("from" in fixed) && "fromId" in fixed) {
|
|
154
|
+
fixed.from = fixed.fromId;
|
|
155
|
+
delete fixed.fromId;
|
|
156
|
+
renamed.push("fromId->from");
|
|
157
|
+
}
|
|
158
|
+
if (!("to" in fixed) && "toId" in fixed) {
|
|
159
|
+
fixed.to = fixed.toId;
|
|
160
|
+
delete fixed.toId;
|
|
161
|
+
renamed.push("toId->to");
|
|
162
|
+
}
|
|
163
|
+
if (renamed.length) {
|
|
164
|
+
repairs.push({ rule: "A10", detail: "renamed " + renamed.join(",") + " on newWires[" + index + "]" });
|
|
165
|
+
}
|
|
166
|
+
return fixed;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Switch rules/wires mismatch — detect, do not repair. The rule
|
|
171
|
+
// alignment is a semantic contract (rules[i] corresponds to output port
|
|
172
|
+
// i); auto-repairing the wrong choice would silently misroute messages.
|
|
173
|
+
// Instead, surface as a targeted retry so the model sees exactly what's
|
|
174
|
+
// wrong and can fix it in one shot.
|
|
175
|
+
// Returns an array of { id, rulesLen, wiresLen } — empty when all clean.
|
|
176
|
+
function detectSwitchMismatches(changes) {
|
|
177
|
+
const mismatches = [];
|
|
178
|
+
(Array.isArray(changes) ? changes : []).forEach(function (entry) {
|
|
179
|
+
if (!entry || !entry.set) { return; }
|
|
180
|
+
const rules = entry.set.rules;
|
|
181
|
+
const wires = entry.set.wires;
|
|
182
|
+
if (Array.isArray(rules) && Array.isArray(wires) && rules.length !== wires.length) {
|
|
183
|
+
mismatches.push({ id: entry.id, rulesLen: rules.length, wiresLen: wires.length });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return mismatches;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Top-level entry point. Takes a parsed envelope object and returns:
|
|
190
|
+
// { envelope, repairs, switchMismatches }
|
|
191
|
+
//
|
|
192
|
+
// - envelope: repaired copy (original not mutated)
|
|
193
|
+
// - repairs: array of { rule, detail } for each repair applied
|
|
194
|
+
// - switchMismatches: array of { id, rulesLen, wiresLen } for switch
|
|
195
|
+
// nodes whose rules/wires arrays have different lengths (targeted retry
|
|
196
|
+
// candidates — callers surface these as skippedNotes or 422 bounces)
|
|
197
|
+
function repairEnvelope(parsed) {
|
|
198
|
+
if (!parsed || typeof parsed !== "object") {
|
|
199
|
+
return { envelope: parsed, repairs: [], switchMismatches: [] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const repairs = [];
|
|
203
|
+
const out = Object.assign({}, parsed);
|
|
204
|
+
|
|
205
|
+
// Flow array (Generate/Build): A1, A2, A3, A4
|
|
206
|
+
if (Array.isArray(out.flow)) {
|
|
207
|
+
out.flow = repairFlowNodes(out.flow, repairs);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// newNodes (Modify): A1, A2, A3, A4
|
|
211
|
+
if (Array.isArray(out.newNodes)) {
|
|
212
|
+
out.newNodes = repairFlowNodes(out.newNodes, repairs);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// changes (Modify): A5, A6, A7, A9
|
|
216
|
+
if (Array.isArray(out.changes)) {
|
|
217
|
+
out.changes = repairChanges(out.changes, out.removeNodes, repairs);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// newWires (Modify): A10
|
|
221
|
+
if (Array.isArray(out.newWires)) {
|
|
222
|
+
out.newWires = repairNewWires(out.newWires, repairs);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Switch alignment check (detect only, not repair)
|
|
226
|
+
const switchMismatches = detectSwitchMismatches(out.changes);
|
|
227
|
+
|
|
228
|
+
return { envelope: out, repairs: repairs, switchMismatches: switchMismatches };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
repairEnvelope: repairEnvelope,
|
|
233
|
+
repairFlowNodes: repairFlowNodes,
|
|
234
|
+
repairChanges: repairChanges,
|
|
235
|
+
repairNewWires: repairNewWires,
|
|
236
|
+
detectSwitchMismatches: detectSwitchMismatches,
|
|
237
|
+
DIFF_SKIP_SERVER: DIFF_SKIP_SERVER
|
|
238
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manny-est/node-red-flowpilot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
|
|
5
5
|
"main": "flowpilot.js",
|
|
6
6
|
"keywords": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/manny-est/flowpilot.git"
|
|
17
|
+
"url": "git+https://github.com/manny-est/flowpilot.git"
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/manny-est/flowpilot#readme",
|
|
20
20
|
"bugs": {
|