@manny-est/node-red-flowpilot 0.4.1 → 0.5.1
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 +42 -0
- package/PROJECT-OVERVIEW.md +7 -7
- package/README.md +14 -8
- package/USER-GUIDE.md +27 -10
- package/flowpilot-core.css +103 -0
- package/flowpilot.js +123 -146
- package/lib/build-core-script.js +44 -0
- package/lib/build-system-prompt.js +6 -0
- package/lib/core/apply-review.js +1926 -0
- package/lib/core/history.js +93 -0
- package/lib/core/init.js +1435 -0
- package/lib/core/main.js +1777 -0
- package/lib/core/markdown.js +165 -0
- package/lib/core/modes.js +1690 -0
- package/lib/core/redaction.js +165 -0
- package/lib/core/selection-context.js +211 -0
- package/lib/envelope.js +136 -0
- package/lib/modify-system-prompt.js +3 -1
- package/lib/provider-openai-compatible.js +112 -15
- package/lib/storage.js +4 -0
- package/package.json +2 -3
- package/flowpilot-core.js +0 -6427
package/flowpilot.js
CHANGED
|
@@ -7,6 +7,8 @@ const documentSystemPrompt = require("./lib/document-system-prompt");
|
|
|
7
7
|
const modifySystemPrompt = require("./lib/modify-system-prompt");
|
|
8
8
|
const buildSystemPrompt = require("./lib/build-system-prompt");
|
|
9
9
|
const personaPrompt = require("./lib/persona-prompt");
|
|
10
|
+
const { buildCoreScript } = require("./lib/build-core-script");
|
|
11
|
+
const { extractJsonObject } = require("./lib/envelope");
|
|
10
12
|
|
|
11
13
|
module.exports = function flowPilotRuntime(RED) {
|
|
12
14
|
const storage = createStorage(RED.settings.userDir);
|
|
@@ -554,13 +556,18 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
554
556
|
|
|
555
557
|
let streamResult;
|
|
556
558
|
try {
|
|
557
|
-
streamResult = await provider.chatStream(activeProvider, messages,
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
559
|
+
streamResult = await provider.chatStream(activeProvider, messages,
|
|
560
|
+
function (delta) {
|
|
561
|
+
const visible = splitter.push(delta);
|
|
562
|
+
if (visible) {
|
|
563
|
+
visibleText += visible;
|
|
564
|
+
res.write("data: " + JSON.stringify({ delta: visible }) + "\n\n");
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
function (reasoningDelta) {
|
|
568
|
+
res.write("data: " + JSON.stringify({ reasoningDelta: reasoningDelta }) + "\n\n");
|
|
562
569
|
}
|
|
563
|
-
|
|
570
|
+
);
|
|
564
571
|
} catch (err) {
|
|
565
572
|
res.write("data: " + JSON.stringify({ error: err.message }) + "\n\n");
|
|
566
573
|
res.end();
|
|
@@ -625,6 +632,11 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
625
632
|
// static lib/debug/view.html that loads the SAME debug-utils.js the
|
|
626
633
|
// sidebar uses).
|
|
627
634
|
//
|
|
635
|
+
// Phase 9 refactor: the SOURCE is now split into lib/core/*.js fragments
|
|
636
|
+
// (see lib/build-core-script.js for why and how), but this route's
|
|
637
|
+
// behavior is unchanged — it still serves one complete script at this
|
|
638
|
+
// same URL, just assembled instead of read off disk verbatim.
|
|
639
|
+
//
|
|
628
640
|
// INTENTIONALLY UNGATED (fixed in 0.4.1 — was needsPermission("settings.
|
|
629
641
|
// read") in 0.4.0, which broke the editor on every adminAuth-enabled
|
|
630
642
|
// instance): these are static client assets, fetched via plain <script
|
|
@@ -639,7 +651,7 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
639
651
|
// routes (settings, chat, generate, modify, etc. below) stay gated.
|
|
640
652
|
|
|
641
653
|
RED.httpAdmin.get("/flowpilot/core.js", function (req, res) {
|
|
642
|
-
res.
|
|
654
|
+
res.type("application/javascript").send(buildCoreScript());
|
|
643
655
|
});
|
|
644
656
|
|
|
645
657
|
RED.httpAdmin.get("/flowpilot/core.css", function (req, res) {
|
|
@@ -744,6 +756,10 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
744
756
|
if (suggestedAction) { body.suggestedAction = suggestedAction; }
|
|
745
757
|
const questionOptions = extractQuestionOptions(chatData);
|
|
746
758
|
if (questionOptions) { body.questionOptions = questionOptions; }
|
|
759
|
+
// Pass reasoning_content through so the frontend can render a thinking
|
|
760
|
+
// block on the non-streaming (agent-loop) path too.
|
|
761
|
+
const rawMsg = result.raw && result.raw.choices && result.raw.choices[0] && result.raw.choices[0].message;
|
|
762
|
+
if (rawMsg && rawMsg.reasoning_content) { body.reasoningContent = rawMsg.reasoning_content; }
|
|
747
763
|
res.json(body);
|
|
748
764
|
} catch (err) {
|
|
749
765
|
storage.appendAudit({ action: "chat_error", error: err.message });
|
|
@@ -926,29 +942,42 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
926
942
|
// probe failure here just means "no tool support", not a /test failure.
|
|
927
943
|
// Persist the result on the provider profile for the agentic tool-calling path.
|
|
928
944
|
const probe = await provider.probeTools(activeProvider);
|
|
945
|
+
const reasoning = provider.detectReasoning(result.raw);
|
|
929
946
|
storage.appendAudit({
|
|
930
947
|
action: "capability_probe",
|
|
931
948
|
providerName: activeProvider.providerName,
|
|
932
949
|
baseUrl: activeProvider.baseUrl,
|
|
933
950
|
model: activeProvider.model,
|
|
934
|
-
supportsTools: probe.supportsTools
|
|
951
|
+
supportsTools: probe.supportsTools,
|
|
952
|
+
isReasoningModel: reasoning.isReasoningModel
|
|
935
953
|
});
|
|
936
954
|
|
|
937
955
|
const updatedProviders = (settings.providers || []).map(function (p) {
|
|
938
956
|
return p.id === activeProvider.id
|
|
939
|
-
? Object.assign({}, p, {
|
|
957
|
+
? Object.assign({}, p, {
|
|
958
|
+
supportsTools: probe.supportsTools,
|
|
959
|
+
toolsProbedAt: new Date().toISOString(),
|
|
960
|
+
isReasoningModel: reasoning.isReasoningModel,
|
|
961
|
+
reasoningProbedAt: new Date().toISOString(),
|
|
962
|
+
probedModel: activeProvider.model
|
|
963
|
+
})
|
|
940
964
|
: p;
|
|
941
965
|
});
|
|
942
966
|
storage.saveSettings(Object.assign({}, settings, { providers: updatedProviders }));
|
|
943
967
|
|
|
968
|
+
const toolLabel = probe.supportsTools
|
|
969
|
+
? "✓ Connected · ✓ Supports tools"
|
|
970
|
+
: "✓ Connected · ⚠ No tool support — compatibility mode";
|
|
971
|
+
const reasoningLabel = reasoning.isReasoningModel ? " · Reasoning model" : "";
|
|
972
|
+
|
|
944
973
|
res.json({
|
|
945
974
|
message: chatMessage || "[No assistant message returned by provider]",
|
|
946
975
|
raw: result.raw ? "[raw response captured]" : null,
|
|
947
976
|
capability: {
|
|
948
977
|
supportsTools: probe.supportsTools,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
978
|
+
isReasoningModel: reasoning.isReasoningModel,
|
|
979
|
+
probedModel: activeProvider.model,
|
|
980
|
+
label: toolLabel + reasoningLabel
|
|
952
981
|
}
|
|
953
982
|
});
|
|
954
983
|
} catch (err) {
|
|
@@ -957,130 +986,63 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
957
986
|
}
|
|
958
987
|
});
|
|
959
988
|
|
|
960
|
-
// ----
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
989
|
+
// ---- Probe: silent capability re-check after model change -----------
|
|
990
|
+
// Called by the frontend when it detects that the active provider's model
|
|
991
|
+
// changed since the last full pre-flight — stale supportsTools silently
|
|
992
|
+
// misroutes chat (agent-loop vs streaming/non-streaming). Runs probeTools
|
|
993
|
+
// + a minimal chat for reasoning detection, saves all results including
|
|
994
|
+
// probedModel, and returns { supportsTools, isReasoningModel, probedModel }.
|
|
965
995
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
// ends (truncated/malformed input).
|
|
971
|
-
function findMatchingBrace(s, startIdx) {
|
|
972
|
-
let depth = 0;
|
|
973
|
-
let inString = false;
|
|
974
|
-
let escaped = false;
|
|
975
|
-
for (let i = startIdx; i < s.length; i++) {
|
|
976
|
-
const ch = s[i];
|
|
977
|
-
if (inString) {
|
|
978
|
-
if (escaped) { escaped = false; }
|
|
979
|
-
else if (ch === "\\") { escaped = true; }
|
|
980
|
-
else if (ch === "\"") { inString = false; }
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
if (ch === "\"") { inString = true; }
|
|
984
|
-
else if (ch === "{") { depth++; }
|
|
985
|
-
else if (ch === "}") {
|
|
986
|
-
depth--;
|
|
987
|
-
if (depth === 0) { return i; }
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
return -1;
|
|
991
|
-
}
|
|
996
|
+
RED.httpAdmin.post("/flowpilot/probe", RED.auth.needsPermission("settings.write"), async function (req, res) {
|
|
997
|
+
try {
|
|
998
|
+
const settings = storage.getSettings();
|
|
999
|
+
const activeProvider = storage.getActiveProvider(settings);
|
|
992
1000
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const firstBrace = s.indexOf("{");
|
|
1000
|
-
const firstBracket = s.indexOf("[");
|
|
1001
|
-
|
|
1002
|
-
// The model occasionally returns a bare top-level array (e.g.
|
|
1003
|
-
// `[ {...node...} ]`) instead of the {explanation, flow} envelope. If we
|
|
1004
|
-
// fell through to the {...} extraction below, indexOf("{")/lastIndexOf("}")
|
|
1005
|
-
// would grab just the first node object — which has no "flow" key and
|
|
1006
|
-
// fails validation. Detect this case up front and wrap it as a minimal
|
|
1007
|
-
// envelope instead.
|
|
1008
|
-
if (firstBracket !== -1 && (firstBrace === -1 || firstBracket < firstBrace)) {
|
|
1009
|
-
const lastBracket = s.lastIndexOf("]");
|
|
1010
|
-
if (lastBracket !== -1 && lastBracket > firstBracket) {
|
|
1011
|
-
try {
|
|
1012
|
-
const arr = JSON.parse(s.slice(firstBracket, lastBracket + 1));
|
|
1013
|
-
if (Array.isArray(arr)) {
|
|
1014
|
-
return { explanation: "", flow: arr };
|
|
1015
|
-
}
|
|
1016
|
-
} catch (e) {
|
|
1017
|
-
// Not a parseable array — fall through to the {...} extraction.
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1001
|
+
const probe = await provider.probeTools(activeProvider);
|
|
1002
|
+
const chatResult = await provider.chat(activeProvider, [
|
|
1003
|
+
{ role: "system", content: "You are a helpful assistant." },
|
|
1004
|
+
{ role: "user", content: "Say hello." }
|
|
1005
|
+
]);
|
|
1006
|
+
const reasoning = provider.detectReasoning(chatResult.raw);
|
|
1021
1007
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1008
|
+
const updatedProviders = (settings.providers || []).map(function (p) {
|
|
1009
|
+
return p.id === activeProvider.id
|
|
1010
|
+
? Object.assign({}, p, {
|
|
1011
|
+
supportsTools: probe.supportsTools,
|
|
1012
|
+
toolsProbedAt: new Date().toISOString(),
|
|
1013
|
+
isReasoningModel: reasoning.isReasoningModel,
|
|
1014
|
+
reasoningProbedAt: new Date().toISOString(),
|
|
1015
|
+
probedModel: activeProvider.model
|
|
1016
|
+
})
|
|
1017
|
+
: p;
|
|
1018
|
+
});
|
|
1019
|
+
storage.saveSettings(Object.assign({}, settings, { providers: updatedProviders }));
|
|
1020
|
+
|
|
1021
|
+
storage.appendAudit({
|
|
1022
|
+
action: "auto_probe",
|
|
1023
|
+
providerName: activeProvider.providerName,
|
|
1024
|
+
baseUrl: activeProvider.baseUrl,
|
|
1025
|
+
model: activeProvider.model,
|
|
1026
|
+
supportsTools: probe.supportsTools,
|
|
1027
|
+
isReasoningModel: reasoning.isReasoningModel
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
res.json({
|
|
1031
|
+
supportsTools: probe.supportsTools,
|
|
1032
|
+
isReasoningModel: reasoning.isReasoningModel,
|
|
1033
|
+
probedModel: activeProvider.model
|
|
1034
|
+
});
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
storage.appendAudit({ action: "auto_probe_error", error: err.message });
|
|
1037
|
+
res.status(500).json({ error: err.message });
|
|
1031
1038
|
}
|
|
1039
|
+
});
|
|
1032
1040
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
// garbage). Try each candidate "{" in order with string-aware brace
|
|
1039
|
-
// matching (findMatchingBrace, which ignores braces inside quoted
|
|
1040
|
-
// strings) rather than just slicing from the first "{" to the last
|
|
1041
|
-
// "}".
|
|
1042
|
-
//
|
|
1043
|
-
// A candidate must not just PARSE, it must also look like one of the
|
|
1044
|
-
// known envelope shapes (have at least one recognized top-level key) —
|
|
1045
|
-
// seen live: a pure-prose advice response that mentioned structured
|
|
1046
|
-
// logging included the illustrative example
|
|
1047
|
-
// `{"level":"info","event":"trivia_answer","user":"alex","correct":true}`,
|
|
1048
|
-
// which IS valid standalone JSON, so the old "first candidate that
|
|
1049
|
-
// parses wins" rule accepted it as "the envelope" and the caller threw
|
|
1050
|
-
// "no recognizable modify fields" — when the right answer was to treat
|
|
1051
|
-
// the whole reply as prose, since there was no real envelope at all.
|
|
1052
|
-
const ENVELOPE_KEYS = ["explanation", "flow", "question", "changes", "newNodes", "newWires", "removeNodes", "newGroups", "prose"];
|
|
1053
|
-
function looksLikeEnvelope(obj) {
|
|
1054
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return false; }
|
|
1055
|
-
return ENVELOPE_KEYS.some(function (k) { return k in obj; });
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
let lastError = null;
|
|
1059
|
-
let searchFrom = firstObjIdx;
|
|
1060
|
-
while (searchFrom !== -1 && searchFrom < s.length) {
|
|
1061
|
-
const end = findMatchingBrace(s, searchFrom);
|
|
1062
|
-
if (end !== -1) {
|
|
1063
|
-
try {
|
|
1064
|
-
const candidate = JSON.parse(s.slice(searchFrom, end + 1));
|
|
1065
|
-
if (looksLikeEnvelope(candidate)) { return candidate; }
|
|
1066
|
-
// Valid JSON, but not envelope-shaped (e.g. an illustrative
|
|
1067
|
-
// example embedded in prose) — keep searching rather than
|
|
1068
|
-
// accepting it.
|
|
1069
|
-
} catch (e) {
|
|
1070
|
-
lastError = e;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
searchFrom = s.indexOf("{", searchFrom + 1);
|
|
1074
|
-
}
|
|
1075
|
-
// No candidate both parsed AND looked like a real envelope — equivalent
|
|
1076
|
-
// to "the model just answered in prose," not "the envelope is broken."
|
|
1077
|
-
// Let callers fall back to rendering this as a normal message instead
|
|
1078
|
-
// of surfacing a parse error (same noJsonFound flag the "no { at all"
|
|
1079
|
-
// branch above uses).
|
|
1080
|
-
const err = lastError || new Error("Provider's JSON object could not be parsed.");
|
|
1081
|
-
err.noJsonFound = true;
|
|
1082
|
-
throw err;
|
|
1083
|
-
}
|
|
1041
|
+
// ---- Generate: produce an importable flow fragment --------------------
|
|
1042
|
+
// Uses the generation system prompt and expects the model to return a single
|
|
1043
|
+
// JSON object { explanation, flow }. This first cut does NOT validate node
|
|
1044
|
+
// types or wire integrity yet (that's the next chunk) — it returns the parsed
|
|
1045
|
+
// envelope so the frontend can display it for review.
|
|
1084
1046
|
|
|
1085
1047
|
// ---------------------------------------------------------------------
|
|
1086
1048
|
// Shared helper: resolve the active provider and assemble the messages
|
|
@@ -1538,29 +1500,42 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
1538
1500
|
// Validate that changes contains no hallucinated ids, and that no id is
|
|
1539
1501
|
// both patched and marked for removal. A group id from contextGroupIds
|
|
1540
1502
|
// is allowed here too (see above) even though it's not in originalIds.
|
|
1503
|
+
// Instead of rejecting the whole response when some ids are bad, drop
|
|
1504
|
+
// only the offending patches and apply the rest — same philosophy as the
|
|
1505
|
+
// applyInsertions partial-failure fix (Phase 8.5 #11). A skippedNote in
|
|
1506
|
+
// the response body tells the user what was dropped and why.
|
|
1541
1507
|
const extraIds = changeIds.filter(function (id) { return !originalIds.has(String(id)) && !contextGroupIds.has(String(id)); });
|
|
1542
1508
|
const wronglyRemovedIds = changeIds.filter(function (id) { return removeSet.has(String(id)); });
|
|
1543
1509
|
|
|
1544
|
-
const
|
|
1545
|
-
if (extraIds.length) {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1510
|
+
const skippedDescriptions = [];
|
|
1511
|
+
if (extraIds.length) {
|
|
1512
|
+
skippedDescriptions.push(
|
|
1513
|
+
extraIds.length === 1
|
|
1514
|
+
? "Skipped 1 change — that node wasn't in your current selection; reselect it to include it"
|
|
1515
|
+
: "Skipped " + extraIds.length + " changes — those nodes weren't in your current selection; reselect them to include them"
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (wronglyRemovedIds.length) {
|
|
1519
|
+
skippedDescriptions.push(
|
|
1520
|
+
wronglyRemovedIds.length === 1
|
|
1521
|
+
? "Skipped 1 change — that node was also marked for removal"
|
|
1522
|
+
: "Skipped " + wronglyRemovedIds.length + " changes — those nodes were also marked for removal"
|
|
1523
|
+
);
|
|
1557
1524
|
}
|
|
1525
|
+
if (skippedDescriptions.length > 0) {
|
|
1526
|
+
storage.appendAudit({ action: "modify_id_mismatch_partial", skipped_extra: extraIds, skipped_conflict: wronglyRemovedIds });
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const badIdSet = new Set(extraIds.map(String).concat(wronglyRemovedIds.map(String)));
|
|
1530
|
+
const validChanges = changes.filter(function (c) {
|
|
1531
|
+
return c && c.id !== undefined && c.id !== null && !badIdSet.has(String(c.id));
|
|
1532
|
+
});
|
|
1558
1533
|
|
|
1559
1534
|
// Each patch's "set" is shallow-merged onto a copy of the original node.
|
|
1560
1535
|
// "id", "x", "y", "z" can never move via a patch — strip them
|
|
1561
1536
|
// defensively even though the prompt already forbids them.
|
|
1562
1537
|
const patchById = {};
|
|
1563
|
-
|
|
1538
|
+
validChanges.forEach(function (c) {
|
|
1564
1539
|
const set = (c.set && typeof c.set === "object") ? c.set : {};
|
|
1565
1540
|
const clean = Object.assign({}, set);
|
|
1566
1541
|
delete clean.id;
|
|
@@ -1587,7 +1562,8 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
1587
1562
|
// is, and findLiveNode() already resolves a group id to the live
|
|
1588
1563
|
// group object (Phase 8.5 C2 slice 1). This is how a group gets
|
|
1589
1564
|
// renamed/restyled — pure property edit, no new apply-side code.
|
|
1590
|
-
const
|
|
1565
|
+
const validChangeIds = validChanges.map(function (c) { return c.id; });
|
|
1566
|
+
const groupPatchIds = validChangeIds.filter(function (id) {
|
|
1591
1567
|
return contextGroupIds.has(String(id)) && !originalIds.has(String(id));
|
|
1592
1568
|
});
|
|
1593
1569
|
groupPatchIds.forEach(function (id) {
|
|
@@ -1698,6 +1674,7 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
1698
1674
|
removeNodes: finalRemoveNodes,
|
|
1699
1675
|
newGroups: newGroups
|
|
1700
1676
|
};
|
|
1677
|
+
if (skippedDescriptions.length > 0) { body.skippedNote = skippedDescriptions.join(". ") + "."; }
|
|
1701
1678
|
if (result.suggestedAction) { body.suggestedAction = result.suggestedAction; }
|
|
1702
1679
|
|
|
1703
1680
|
return { status: 200, body: body };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// flowpilot-core.js is loaded directly in the BROWSER via a single
|
|
7
|
+
// <script src="flowpilot/core.js"> tag - one big IIFE, no module system,
|
|
8
|
+
// every function/var sharing one closure by lexical scope (not via
|
|
9
|
+
// require/import). Phase 9's refactor splits the SOURCE into focused files
|
|
10
|
+
// under lib/core/ for maintainability, but the served SCRIPT must stay
|
|
11
|
+
// exactly what it was: one concatenated text, so cross-fragment references
|
|
12
|
+
// keep working unmodified and script-loading/timing behavior never changes.
|
|
13
|
+
//
|
|
14
|
+
// Order matters only for top-level code that runs immediately at load time
|
|
15
|
+
// (e.g. a `var x = (function(){...})();` initializer) - plain function
|
|
16
|
+
// declarations are hoisted within the shared closure and safe in any
|
|
17
|
+
// order. Keep new fragments appended in the same relative order they held
|
|
18
|
+
// in the original single file unless a specific dependency says otherwise.
|
|
19
|
+
const FRAGMENT_ORDER = [
|
|
20
|
+
"redaction.js",
|
|
21
|
+
"history.js",
|
|
22
|
+
"markdown.js",
|
|
23
|
+
"selection-context.js",
|
|
24
|
+
"apply-review.js",
|
|
25
|
+
"modes.js",
|
|
26
|
+
"main.js",
|
|
27
|
+
"init.js"
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const HEADER = "(function () {\n \"use strict\";\n";
|
|
31
|
+
const FOOTER = "\n})();\n";
|
|
32
|
+
|
|
33
|
+
let cached = null;
|
|
34
|
+
|
|
35
|
+
function buildCoreScript() {
|
|
36
|
+
if (cached) { return cached; }
|
|
37
|
+
const body = FRAGMENT_ORDER.map(function (name) {
|
|
38
|
+
return fs.readFileSync(path.join(__dirname, "core", name), "utf8");
|
|
39
|
+
}).join("\n");
|
|
40
|
+
cached = HEADER + body + FOOTER;
|
|
41
|
+
return cached;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { buildCoreScript: buildCoreScript };
|
|
@@ -25,6 +25,12 @@ Example "explanation" for a trivial goal:
|
|
|
25
25
|
|
|
26
26
|
Deploy and trigger it; let me know what the debug output shows."
|
|
27
27
|
|
|
28
|
+
ALWAYS include at least one debug node in your proposed flow so the test-and-review cycle has something to observe. This is REQUIRED, not optional. Specific rules:
|
|
29
|
+
- HTTP endpoint flows (http in → function/change → http response): add a debug node tapping the function/change output BEFORE the http response node — wire the function/change output to BOTH the debug node AND the http response node.
|
|
30
|
+
- Inject-triggered flows: the debug node at the end is fine.
|
|
31
|
+
- Any other flow shape: add a debug node at the last meaningful output point.
|
|
32
|
+
Never generate a build flow without a debug node. The loop cannot review what it cannot see.
|
|
33
|
+
|
|
28
34
|
Everything below describes the envelope/rules for THIS step specifically — they work exactly as written, including the parts that say "Generate mode": for the purposes of this prompt, treat that phrase as describing this build step, not a separate mode. The "explanation" field's content rules below still apply — your "Plan:" block comes first, then that content follows immediately after it in the same field.
|
|
29
35
|
|
|
30
36
|
---
|