@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.
@@ -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.0",
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": {