@kairos-sdk/core 0.3.2 → 0.4.5
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/README.md +46 -11
- package/dist/{chunk-KQSNT3HZ.js → chunk-4TS6GW6O.js} +148 -368
- package/dist/chunk-4TS6GW6O.js.map +1 -0
- package/dist/chunk-6CLI43FI.js +315 -0
- package/dist/chunk-6CLI43FI.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/{chunk-RYGYNOR6.js → chunk-6IXW3WCC.js} +936 -412
- package/dist/chunk-6IXW3WCC.js.map +1 -0
- package/dist/chunk-CR2NHLOH.js +523 -0
- package/dist/chunk-CR2NHLOH.js.map +1 -0
- package/dist/cli.cjs +1402 -170
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +140 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1262 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -537
- package/dist/index.d.ts +5 -537
- package/dist/index.js +8 -4
- package/dist/mcp-server.cjs +1259 -129
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +113 -8
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-CpUcHhKW.d.cts +566 -0
- package/dist/reader-CpUcHhKW.d.ts +566 -0
- package/dist/standalone.cjs +2460 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +105 -0
- package/dist/standalone.d.ts +105 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +6 -1
- package/dist/chunk-KQSNT3HZ.js.map +0 -1
- package/dist/chunk-RYGYNOR6.js.map +0 -1
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/utils/uuid.ts
|
|
2
|
+
function generateUUID() {
|
|
3
|
+
return crypto.randomUUID();
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
// src/errors/base.ts
|
|
2
7
|
var KairosError = class extends Error {
|
|
3
8
|
constructor(message, cause) {
|
|
@@ -397,17 +402,31 @@ var N8nValidator = class {
|
|
|
397
402
|
this.checkRule21(workflow, issues);
|
|
398
403
|
this.checkRule22(workflow, issues);
|
|
399
404
|
this.checkRule23(workflow, issues);
|
|
405
|
+
this.checkRule24(workflow, issues);
|
|
406
|
+
this.checkRule25(workflow, issues);
|
|
407
|
+
this.checkRule26(workflow, issues);
|
|
408
|
+
if (Array.isArray(workflow.nodes)) {
|
|
409
|
+
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
410
|
+
for (const issue of issues) {
|
|
411
|
+
if (issue.nodeId && !issue.nodeType) {
|
|
412
|
+
const nt = nodeById.get(issue.nodeId);
|
|
413
|
+
if (nt) issue.nodeType = nt;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
400
417
|
const errors = issues.filter((i) => i.severity === "error");
|
|
401
418
|
return { valid: errors.length === 0, issues };
|
|
402
419
|
}
|
|
403
|
-
err(issues, rule, message, nodeId) {
|
|
420
|
+
err(issues, rule, message, nodeId, nodeType) {
|
|
404
421
|
const issue = { rule, severity: "error", message };
|
|
405
422
|
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
423
|
+
if (nodeType !== void 0) issue.nodeType = nodeType;
|
|
406
424
|
issues.push(issue);
|
|
407
425
|
}
|
|
408
|
-
warn(issues, rule, message, nodeId) {
|
|
426
|
+
warn(issues, rule, message, nodeId, nodeType) {
|
|
409
427
|
const issue = { rule, severity: "warn", message };
|
|
410
428
|
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
429
|
+
if (nodeType !== void 0) issue.nodeType = nodeType;
|
|
411
430
|
issues.push(issue);
|
|
412
431
|
}
|
|
413
432
|
isTriggerNode(node) {
|
|
@@ -518,10 +537,14 @@ var N8nValidator = class {
|
|
|
518
537
|
checkRule11(w, issues) {
|
|
519
538
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
520
539
|
const reachable = /* @__PURE__ */ new Set();
|
|
521
|
-
|
|
540
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
541
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
522
542
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
523
|
-
|
|
543
|
+
let hasAiPort = false;
|
|
544
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
524
545
|
if (!Array.isArray(portGroup)) continue;
|
|
546
|
+
const isAiPort = portName.startsWith("ai_");
|
|
547
|
+
if (isAiPort) hasAiPort = true;
|
|
525
548
|
for (const targets of portGroup) {
|
|
526
549
|
if (!Array.isArray(targets)) continue;
|
|
527
550
|
for (const target of targets) {
|
|
@@ -530,10 +553,13 @@ var N8nValidator = class {
|
|
|
530
553
|
}
|
|
531
554
|
}
|
|
532
555
|
}
|
|
556
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
533
557
|
}
|
|
534
558
|
for (const node of w.nodes) {
|
|
535
559
|
if (node.type.includes("stickyNote")) continue;
|
|
536
|
-
if (
|
|
560
|
+
if (this.isTriggerNode(node)) continue;
|
|
561
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
562
|
+
if (!reachable.has(node.name)) {
|
|
537
563
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
538
564
|
}
|
|
539
565
|
}
|
|
@@ -729,6 +755,76 @@ var N8nValidator = class {
|
|
|
729
755
|
}
|
|
730
756
|
}
|
|
731
757
|
}
|
|
758
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
759
|
+
checkRule24(w, issues) {
|
|
760
|
+
if (!Array.isArray(w.nodes)) return;
|
|
761
|
+
const deprecated = /\$node\s*\[/;
|
|
762
|
+
for (const node of w.nodes) {
|
|
763
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
764
|
+
if (deprecated.test(expr)) {
|
|
765
|
+
this.warn(
|
|
766
|
+
issues,
|
|
767
|
+
24,
|
|
768
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
769
|
+
node.id
|
|
770
|
+
);
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
777
|
+
checkRule25(w, issues) {
|
|
778
|
+
if (!Array.isArray(w.nodes)) return;
|
|
779
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
780
|
+
for (const node of w.nodes) {
|
|
781
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
782
|
+
if (itemIndex.test(expr)) {
|
|
783
|
+
this.warn(
|
|
784
|
+
issues,
|
|
785
|
+
25,
|
|
786
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
787
|
+
node.id
|
|
788
|
+
);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
795
|
+
checkRule26(w, issues) {
|
|
796
|
+
if (!Array.isArray(w.nodes)) return;
|
|
797
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
798
|
+
for (const node of w.nodes) {
|
|
799
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
800
|
+
if (bareRef.test(expr)) {
|
|
801
|
+
this.warn(
|
|
802
|
+
issues,
|
|
803
|
+
26,
|
|
804
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
805
|
+
node.id
|
|
806
|
+
);
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
extractExpressions(params) {
|
|
813
|
+
const expressions = [];
|
|
814
|
+
const walk = (val) => {
|
|
815
|
+
if (typeof val === "string") {
|
|
816
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
817
|
+
expressions.push(val);
|
|
818
|
+
}
|
|
819
|
+
} else if (Array.isArray(val)) {
|
|
820
|
+
for (const item of val) walk(item);
|
|
821
|
+
} else if (val !== null && typeof val === "object") {
|
|
822
|
+
for (const v of Object.values(val)) walk(v);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
walk(params);
|
|
826
|
+
return expressions;
|
|
827
|
+
}
|
|
732
828
|
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
733
829
|
checkRule21(w, issues) {
|
|
734
830
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -752,348 +848,93 @@ var N8nValidator = class {
|
|
|
752
848
|
}
|
|
753
849
|
};
|
|
754
850
|
|
|
755
|
-
// src/
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
851
|
+
// src/telemetry/collector.ts
|
|
852
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
853
|
+
import { join } from "path";
|
|
854
|
+
import { homedir } from "os";
|
|
759
855
|
|
|
760
|
-
|
|
761
|
-
|
|
856
|
+
// src/telemetry/types.ts
|
|
857
|
+
var TELEMETRY_SCHEMA_VERSION = 2;
|
|
762
858
|
|
|
763
|
-
|
|
764
|
-
{
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
"
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
859
|
+
// src/telemetry/collector.ts
|
|
860
|
+
var TelemetryCollector = class {
|
|
861
|
+
dir;
|
|
862
|
+
sessionId;
|
|
863
|
+
dirReady = null;
|
|
864
|
+
constructor(dir) {
|
|
865
|
+
this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
|
|
866
|
+
this.sessionId = generateUUID();
|
|
867
|
+
}
|
|
868
|
+
async emit(eventType, data, runId) {
|
|
869
|
+
const event = {
|
|
870
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
871
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
872
|
+
sessionId: this.sessionId,
|
|
873
|
+
...runId ? { runId } : {},
|
|
874
|
+
eventType,
|
|
875
|
+
data
|
|
876
|
+
};
|
|
877
|
+
if (!this.dirReady) {
|
|
878
|
+
this.dirReady = mkdir(this.dir, { recursive: true }).then(() => {
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
await this.dirReady;
|
|
882
|
+
const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
|
|
883
|
+
const filepath = join(this.dir, filename);
|
|
884
|
+
await appendFile(filepath, JSON.stringify(event) + "\n", "utf-8");
|
|
776
885
|
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
### Node IDs:
|
|
780
|
-
- Every node.id must be a valid UUID v4 (random hex, format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
|
781
|
-
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
782
|
-
|
|
783
|
-
### Credentials:
|
|
784
|
-
- Only reference credentials with exact type names (see catalog below)
|
|
785
|
-
- If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
|
|
786
|
-
- Never put API keys or tokens in parameters when a credential type exists
|
|
787
|
-
|
|
788
|
-
### Node names:
|
|
789
|
-
- All node names must be unique within the workflow
|
|
790
|
-
- Use descriptive names: "Fetch Open Invoices" not "HTTP Request 2"
|
|
791
|
-
|
|
792
|
-
### Positioning:
|
|
793
|
-
- Trigger node: [250, 300]
|
|
794
|
-
- Each subsequent step: x + 220 minimum
|
|
795
|
-
- Parallel branches: offset y by \xB1150
|
|
796
|
-
- AI sub-nodes: place below their root node (y + 200)
|
|
797
|
-
|
|
798
|
-
---
|
|
799
|
-
|
|
800
|
-
## CONNECTION RULES \u2014 the most common source of errors
|
|
801
|
-
|
|
802
|
-
### Standard connections (main data flow):
|
|
803
|
-
"NodeA": { "main": [ [ { "node": "NodeB", "type": "main", "index": 0 } ] ] }
|
|
804
|
-
|
|
805
|
-
### AI connections \u2014 CRITICAL: the SUB-NODE is the SOURCE, NOT the agent/chain:
|
|
806
|
-
"OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent", "type": "ai_languageModel", "index": 0 } ] ] }
|
|
807
|
-
"Simple Memory": { "ai_memory": [ [ { "node": "AI Agent", "type": "ai_memory", "index": 0 } ] ] }
|
|
808
|
-
"Calculator Tool": { "ai_tool": [ [ { "node": "AI Agent", "type": "ai_tool", "index": 0 } ] ] }
|
|
809
|
-
|
|
810
|
-
The AI Agent node does NOT appear in connections as a source for ai_* types.
|
|
811
|
-
Every AI Agent must have at least one ai_languageModel sub-node connected.
|
|
812
|
-
|
|
813
|
-
### IF node \u2014 two output ports (0 = true, 1 = false):
|
|
814
|
-
"IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
|
|
815
|
-
|
|
816
|
-
### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
|
|
817
|
-
Connect output 0 to the node that runs AFTER all batches complete.
|
|
818
|
-
Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
|
|
819
|
-
|
|
820
|
-
### Webhook + RespondToWebhook pattern:
|
|
821
|
-
When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
|
|
822
|
-
"Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
|
|
823
|
-
"Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
|
|
824
|
-
|
|
825
|
-
### Triggers have no incoming connections.
|
|
826
|
-
### Connection keys are NODE NAMES, never node IDs.
|
|
827
|
-
|
|
828
|
-
### Nested parameters:
|
|
829
|
-
Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
|
|
830
|
-
|
|
831
|
-
---
|
|
832
|
-
|
|
833
|
-
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
834
|
-
|
|
835
|
-
### Triggers (always at least one required):
|
|
836
|
-
n8n-nodes-base.manualTrigger typeVersion: 1 \u2014 testing only
|
|
837
|
-
n8n-nodes-base.scheduleTrigger typeVersion: 1.2 \u2014 params: rule.interval[{field, ...}]
|
|
838
|
-
n8n-nodes-base.webhook typeVersion: 2 \u2014 params: httpMethod, path, responseMode
|
|
839
|
-
n8n-nodes-base.formTrigger typeVersion: 2.2
|
|
840
|
-
n8n-nodes-base.emailReadImap typeVersion: 2 \u2014 cred: imap
|
|
841
|
-
n8n-nodes-base.errorTrigger typeVersion: 1
|
|
842
|
-
n8n-nodes-base.executeWorkflowTrigger typeVersion: 1.1
|
|
843
|
-
n8n-nodes-base.gmailTrigger typeVersion: 1.2 \u2014 cred: gmailOAuth2
|
|
844
|
-
n8n-nodes-base.slackTrigger typeVersion: 1 \u2014 cred: slackApi
|
|
845
|
-
n8n-nodes-base.telegramTrigger typeVersion: 1.2 \u2014 cred: telegramApi
|
|
846
|
-
n8n-nodes-base.githubTrigger typeVersion: 1 \u2014 cred: githubApi
|
|
847
|
-
n8n-nodes-base.airtableTrigger typeVersion: 1 \u2014 cred: airtableTokenApi
|
|
848
|
-
n8n-nodes-base.notionTrigger typeVersion: 1 \u2014 cred: notionApi
|
|
849
|
-
@n8n/n8n-nodes-langchain.chatTrigger typeVersion: 1.1 \u2014 pairs with AI Agent
|
|
850
|
-
|
|
851
|
-
### Core logic:
|
|
852
|
-
n8n-nodes-base.code typeVersion: 2 \u2014 params: mode, jsCode
|
|
853
|
-
n8n-nodes-base.httpRequest typeVersion: 4.2 \u2014 params: method, url, [sendBody, jsonBody, sendHeaders, headerParameters]
|
|
854
|
-
n8n-nodes-base.set typeVersion: 3.4 \u2014 params: assignments.assignments[{id, name, value, type}]
|
|
855
|
-
n8n-nodes-base.if typeVersion: 2.2 \u2014 params: conditions.conditions[{id, leftValue, rightValue, operator}], combinator
|
|
856
|
-
n8n-nodes-base.switch typeVersion: 3.2 \u2014 multi-branch routing
|
|
857
|
-
n8n-nodes-base.filter typeVersion: 2.2 \u2014 params: conditions (same as IF), 1 output
|
|
858
|
-
n8n-nodes-base.merge typeVersion: 3 \u2014 modes: append/combine/chooseBranch
|
|
859
|
-
n8n-nodes-base.splitInBatches typeVersion: 3 \u2014 output 0=done, output 1=loop body
|
|
860
|
-
n8n-nodes-base.wait typeVersion: 1.1
|
|
861
|
-
n8n-nodes-base.executeWorkflow typeVersion: 1.2
|
|
862
|
-
n8n-nodes-base.respondToWebhook typeVersion: 1.1 \u2014 required when webhook responseMode is "responseNode"
|
|
863
|
-
n8n-nodes-base.noOp typeVersion: 1
|
|
864
|
-
n8n-nodes-base.splitOut typeVersion: 1
|
|
865
|
-
n8n-nodes-base.aggregate typeVersion: 1
|
|
866
|
-
n8n-nodes-base.stickyNote typeVersion: 1 \u2014 never connected, canvas annotation only
|
|
867
|
-
|
|
868
|
-
### Email / messaging:
|
|
869
|
-
n8n-nodes-base.emailSend typeVersion: 2.1 \u2014 cred: smtp
|
|
870
|
-
n8n-nodes-base.slack typeVersion: 2.2 \u2014 cred: slackOAuth2Api \u2014 params: resource, operation, select, channelId{__rl}, text
|
|
871
|
-
n8n-nodes-base.telegram typeVersion: 1.2 \u2014 cred: telegramApi
|
|
872
|
-
n8n-nodes-base.discord typeVersion: 2 \u2014 cred: discordWebhookApi
|
|
873
|
-
|
|
874
|
-
### Google:
|
|
875
|
-
n8n-nodes-base.gmail typeVersion: 2.1 \u2014 cred: gmailOAuth2 \u2014 params: resource, operation
|
|
876
|
-
n8n-nodes-base.googleSheets typeVersion: 4.5 \u2014 cred: googleSheetsOAuth2Api \u2014 params: resource, operation, documentId{__rl}, sheetName{__rl}
|
|
877
|
-
n8n-nodes-base.googleDrive typeVersion: 3 \u2014 cred: googleDriveOAuth2Api
|
|
878
|
-
n8n-nodes-base.googleCalendar typeVersion: 1.3 \u2014 cred: googleCalendarOAuth2Api
|
|
879
|
-
|
|
880
|
-
### Productivity:
|
|
881
|
-
n8n-nodes-base.notion typeVersion: 2.2 \u2014 cred: notionApi
|
|
882
|
-
n8n-nodes-base.airtable typeVersion: 2.1 \u2014 cred: airtableTokenApi
|
|
883
|
-
n8n-nodes-base.github typeVersion: 1.1 \u2014 cred: githubApi
|
|
884
|
-
n8n-nodes-base.jira typeVersion: 1 \u2014 cred: jiraSoftwareCloudApi
|
|
885
|
-
n8n-nodes-base.hubspot typeVersion: 2.1 \u2014 cred: hubspotOAuth2Api
|
|
886
|
-
|
|
887
|
-
### Databases:
|
|
888
|
-
n8n-nodes-base.postgres typeVersion: 2.5 \u2014 cred: postgres
|
|
889
|
-
n8n-nodes-base.mySql typeVersion: 2.4 \u2014 cred: mySql
|
|
890
|
-
n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
|
|
891
|
-
n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
|
|
892
|
-
n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
|
|
893
|
-
|
|
894
|
-
### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
|
|
895
|
-
@n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
|
|
896
|
-
@n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
|
|
897
|
-
@n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
|
|
898
|
-
@n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
|
|
899
|
-
@n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
|
|
900
|
-
|
|
901
|
-
### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
|
|
902
|
-
@n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
|
|
903
|
-
@n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
|
|
904
|
-
@n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
|
|
905
|
-
@n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
|
|
906
|
-
@n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
|
|
907
|
-
@n8n/n8n-nodes-langchain.toolCode typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
908
|
-
@n8n/n8n-nodes-langchain.toolHttpRequest typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
909
|
-
@n8n/n8n-nodes-langchain.toolCalculator typeVersion: 1 \u2014 \u2014 ai_tool
|
|
910
|
-
|
|
911
|
-
### Resource locator (__rl) format (Google / Slack / Notion modern nodes):
|
|
912
|
-
{ "__rl": true, "mode": "id", "value": "ACTUAL_ID" }
|
|
913
|
-
{ "__rl": true, "mode": "name", "value": "#channel-name" }
|
|
914
|
-
|
|
915
|
-
### App node parameter pattern:
|
|
916
|
-
{ "resource": "message", "operation": "send", ...operation-specific fields }
|
|
917
|
-
|
|
918
|
-
### Schedule Trigger \u2014 daily at 9am example:
|
|
919
|
-
{ "rule": { "interval": [{ "field": "days", "daysInterval": 1, "triggerAtHour": 9, "triggerAtMinute": 0 }] } }
|
|
920
|
-
Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] } }
|
|
921
|
-
|
|
922
|
-
---
|
|
923
|
-
|
|
924
|
-
## PRE-DELIVERY SELF-CHECK (do this before calling the tool):
|
|
925
|
-
1. Every connection source/target name exists in nodes array
|
|
926
|
-
2. No duplicate node names
|
|
927
|
-
3. No duplicate node IDs
|
|
928
|
-
4. No forbidden fields at the workflow root
|
|
929
|
-
5. At least one trigger node present
|
|
930
|
-
6. Every AI Agent has an ai_languageModel sub-node
|
|
931
|
-
7. settings block is complete with executionOrder: "v1"
|
|
932
|
-
|
|
933
|
-
---
|
|
934
|
-
|
|
935
|
-
Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
|
|
936
|
-
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
937
|
-
|
|
938
|
-
// src/utils/thresholds.ts
|
|
939
|
-
var DIRECT_THRESHOLD = 0.92;
|
|
940
|
-
var REFERENCE_THRESHOLD = 0.72;
|
|
941
|
-
function scoreToMode(score) {
|
|
942
|
-
if (score >= DIRECT_THRESHOLD) return "direct";
|
|
943
|
-
if (score >= REFERENCE_THRESHOLD) return "reference";
|
|
944
|
-
return "scratch";
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// src/generation/prompt-builder.ts
|
|
948
|
-
var RULE_REMEDIES = {
|
|
949
|
-
1: "Provide a non-empty workflow name string",
|
|
950
|
-
2: "Include at least one node in the nodes array",
|
|
951
|
-
3: "Every node must have a unique UUID v4 string as its id field",
|
|
952
|
-
4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
|
|
953
|
-
5: "Every node must have a non-empty type string",
|
|
954
|
-
6: "Every node must have a positive integer typeVersion",
|
|
955
|
-
7: "Every node must have a position array of exactly [x, y] numbers",
|
|
956
|
-
8: "Every node must have a non-empty name string",
|
|
957
|
-
9: "connections must be a plain object (use {} if no connections)",
|
|
958
|
-
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
959
|
-
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
960
|
-
14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
|
|
961
|
-
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
962
|
-
16: "All node names must be unique within the workflow",
|
|
963
|
-
17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
|
|
964
|
-
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
965
|
-
19: "Use known safe typeVersion values for each node type",
|
|
966
|
-
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
967
|
-
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
968
|
-
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
|
|
969
886
|
};
|
|
970
|
-
var PromptBuilder = class {
|
|
971
|
-
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
972
|
-
const mode = this.resolveMode(matches);
|
|
973
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
974
|
-
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
975
|
-
return { system, userMessage, mode, matches };
|
|
976
|
-
}
|
|
977
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
978
|
-
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
979
|
-
return `${base}
|
|
980
887
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
|
|
985
|
-
}
|
|
986
|
-
resolveMode(matches) {
|
|
987
|
-
if (matches.length === 0) return "scratch";
|
|
988
|
-
const top = matches[0];
|
|
989
|
-
if (!top) return "scratch";
|
|
990
|
-
return scoreToMode(top.score);
|
|
991
|
-
}
|
|
992
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
993
|
-
let basePrompt = SYSTEM_PROMPT_V1;
|
|
994
|
-
if (dynamicCatalog) {
|
|
995
|
-
basePrompt = basePrompt.replace(
|
|
996
|
-
/## NODE CATALOG — exact type strings and safe typeVersions[\s\S]*?(?=## PRE-DELIVERY SELF-CHECK)/,
|
|
997
|
-
dynamicCatalog + "\n\n"
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
const blocks = [
|
|
1001
|
-
{
|
|
1002
|
-
type: "text",
|
|
1003
|
-
text: basePrompt,
|
|
1004
|
-
cache_control: { type: "ephemeral" }
|
|
1005
|
-
}
|
|
1006
|
-
];
|
|
1007
|
-
if (mode === "reference" && matches.length > 0) {
|
|
1008
|
-
const refText = matches.slice(0, 3).map((m) => {
|
|
1009
|
-
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1010
|
-
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1011
|
-
Nodes:
|
|
1012
|
-
${nodes}`;
|
|
1013
|
-
}).join("\n\n");
|
|
1014
|
-
blocks.push({
|
|
1015
|
-
type: "text",
|
|
1016
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1017
|
-
|
|
1018
|
-
${refText}`
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
if (mode === "direct" && matches[0]) {
|
|
1022
|
-
const match = matches[0];
|
|
1023
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1024
|
-
if (json.length > 3e4) {
|
|
1025
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1026
|
-
blocks.push({
|
|
1027
|
-
type: "text",
|
|
1028
|
-
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1029
|
-
Nodes:
|
|
1030
|
-
${nodes}`
|
|
1031
|
-
});
|
|
1032
|
-
} else {
|
|
1033
|
-
blocks.push({
|
|
1034
|
-
type: "text",
|
|
1035
|
-
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
888
|
+
// src/telemetry/reader.ts
|
|
889
|
+
import { homedir as homedir2 } from "os";
|
|
890
|
+
import { join as join3 } from "path";
|
|
1036
891
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
892
|
+
// src/telemetry/event-reader.ts
|
|
893
|
+
import { readdir } from "fs/promises";
|
|
894
|
+
import { createReadStream } from "fs";
|
|
895
|
+
import { join as join2 } from "path";
|
|
896
|
+
import { createInterface } from "readline";
|
|
897
|
+
async function readTelemetryEvents(dir, days) {
|
|
898
|
+
let files;
|
|
899
|
+
try {
|
|
900
|
+
files = await readdir(dir);
|
|
901
|
+
} catch {
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
905
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
906
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
907
|
+
const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
908
|
+
const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
909
|
+
const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
|
|
910
|
+
const events = [];
|
|
911
|
+
for (const file of recentFiles) {
|
|
912
|
+
const fileDate = file.replace(".jsonl", "");
|
|
913
|
+
try {
|
|
914
|
+
const rl = createInterface({
|
|
915
|
+
input: createReadStream(join2(dir, file), "utf-8"),
|
|
916
|
+
crlfDelay: Infinity
|
|
1048
917
|
});
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
buildFailureWarnings(matches, globalFailureRates) {
|
|
1057
|
-
const lines = [];
|
|
1058
|
-
for (const match of matches) {
|
|
1059
|
-
const patterns = match.workflow.failurePatterns;
|
|
1060
|
-
if (!patterns?.length) continue;
|
|
1061
|
-
for (const fp of patterns) {
|
|
1062
|
-
const remedy = RULE_REMEDIES[fp.rule];
|
|
1063
|
-
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1064
|
-
lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
|
|
918
|
+
for await (const line of rl) {
|
|
919
|
+
if (!line.trim()) continue;
|
|
920
|
+
try {
|
|
921
|
+
events.push({ ...JSON.parse(line), fileDate });
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
1065
924
|
}
|
|
925
|
+
} catch {
|
|
1066
926
|
}
|
|
1067
|
-
const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
|
|
1068
|
-
for (const rule of highFreqRules) {
|
|
1069
|
-
const remedy = RULE_REMEDIES[rule.rule];
|
|
1070
|
-
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1071
|
-
lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
|
|
1072
|
-
}
|
|
1073
|
-
if (lines.length === 0) return null;
|
|
1074
|
-
const unique = [...new Set(lines)];
|
|
1075
|
-
return `## Known Failure Patterns \u2014 AVOID THESE
|
|
1076
|
-
|
|
1077
|
-
Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
|
|
1078
|
-
${unique.join("\n")}`;
|
|
1079
927
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
Workflow name: "${request.name}"` : "";
|
|
1083
|
-
return `Build a workflow that: ${request.description}${namePart}`;
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
928
|
+
return events;
|
|
929
|
+
}
|
|
1086
930
|
|
|
1087
931
|
// src/telemetry/reader.ts
|
|
1088
|
-
import { readFile, readdir } from "fs/promises";
|
|
1089
|
-
import { join } from "path";
|
|
1090
|
-
import { homedir } from "os";
|
|
1091
932
|
var TelemetryReader = class {
|
|
1092
933
|
dir;
|
|
1093
934
|
cache = null;
|
|
1094
935
|
cacheTime = 0;
|
|
1095
936
|
constructor(dir) {
|
|
1096
|
-
this.dir = dir ??
|
|
937
|
+
this.dir = dir ?? join3(homedir2(), ".kairos", "telemetry");
|
|
1097
938
|
}
|
|
1098
939
|
async getFailureRates(days = 30) {
|
|
1099
940
|
const now = Date.now();
|
|
@@ -1102,9 +943,10 @@ var TelemetryReader = class {
|
|
|
1102
943
|
}
|
|
1103
944
|
const events = await this.readRecentEvents(days);
|
|
1104
945
|
const buildSessions = new Set(
|
|
1105
|
-
events.filter((e) => e.eventType === "build_complete"
|
|
946
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
1106
947
|
);
|
|
1107
|
-
|
|
948
|
+
const MIN_BUILDS_FOR_RATES = 3;
|
|
949
|
+
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1108
950
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1109
951
|
for (const event of events) {
|
|
1110
952
|
if (event.eventType !== "generation_attempt") continue;
|
|
@@ -1142,32 +984,566 @@ var TelemetryReader = class {
|
|
|
1142
984
|
return rates;
|
|
1143
985
|
}
|
|
1144
986
|
async readRecentEvents(days) {
|
|
1145
|
-
|
|
987
|
+
return readTelemetryEvents(this.dir, days);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// src/telemetry/pattern-analyzer.ts
|
|
992
|
+
import { writeFile, readFile as fsReadFile, appendFile as appendFile2, mkdir as mkdir2, rename } from "fs/promises";
|
|
993
|
+
import { join as join4 } from "path";
|
|
994
|
+
import { homedir as homedir3 } from "os";
|
|
995
|
+
|
|
996
|
+
// src/validation/rule-metadata.ts
|
|
997
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
|
|
998
|
+
var RULE_PIPELINE_STAGES = {
|
|
999
|
+
1: "node_generation",
|
|
1000
|
+
2: "node_generation",
|
|
1001
|
+
3: "node_generation",
|
|
1002
|
+
4: "node_generation",
|
|
1003
|
+
5: "node_generation",
|
|
1004
|
+
6: "node_generation",
|
|
1005
|
+
7: "node_generation",
|
|
1006
|
+
8: "node_generation",
|
|
1007
|
+
9: "connection_wiring",
|
|
1008
|
+
10: "connection_wiring",
|
|
1009
|
+
11: "connection_wiring",
|
|
1010
|
+
12: "workflow_structure",
|
|
1011
|
+
13: "node_generation",
|
|
1012
|
+
14: "workflow_structure",
|
|
1013
|
+
15: "node_generation",
|
|
1014
|
+
16: "node_generation",
|
|
1015
|
+
17: "credential_injection",
|
|
1016
|
+
18: "connection_wiring",
|
|
1017
|
+
19: "node_generation",
|
|
1018
|
+
20: "connection_wiring",
|
|
1019
|
+
21: "workflow_structure",
|
|
1020
|
+
22: "workflow_structure",
|
|
1021
|
+
23: "node_generation",
|
|
1022
|
+
24: "expression_syntax",
|
|
1023
|
+
25: "expression_syntax",
|
|
1024
|
+
26: "expression_syntax"
|
|
1025
|
+
};
|
|
1026
|
+
var RULE_EXAMPLES = {
|
|
1027
|
+
17: {
|
|
1028
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1029
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1030
|
+
},
|
|
1031
|
+
24: {
|
|
1032
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1033
|
+
good: "$('Fetch Data').item.json.email"
|
|
1034
|
+
},
|
|
1035
|
+
25: {
|
|
1036
|
+
bad: "$json.items[0].email",
|
|
1037
|
+
good: "$json.email"
|
|
1038
|
+
},
|
|
1039
|
+
26: {
|
|
1040
|
+
bad: "$('Fetch Data').json.email",
|
|
1041
|
+
good: "$('Fetch Data').first().json.email"
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
var RULE_MITIGATIONS = {
|
|
1045
|
+
1: "Provide a non-empty workflow name string",
|
|
1046
|
+
2: "Include at least one node in the nodes array",
|
|
1047
|
+
3: "Every node must have a unique UUID v4 string as its id field",
|
|
1048
|
+
4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
|
|
1049
|
+
5: "Every node must have a non-empty type string",
|
|
1050
|
+
6: "Every node must have a positive integer typeVersion",
|
|
1051
|
+
7: "Every node must have a position array of exactly [x, y] numbers",
|
|
1052
|
+
8: "Every node must have a non-empty name string",
|
|
1053
|
+
9: "connections must be a plain object (use {} if no connections)",
|
|
1054
|
+
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
1055
|
+
11: "Every non-trigger node should have at least one incoming connection",
|
|
1056
|
+
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
1057
|
+
13: "workflow.settings must be a plain object if present",
|
|
1058
|
+
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1059
|
+
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1060
|
+
16: "All node names must be unique within the workflow",
|
|
1061
|
+
17: 'Each credential entry must be keyed by credential type with an object value: { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Credential" } } \u2014 the key is the credential type, the value has id and name strings',
|
|
1062
|
+
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1063
|
+
19: "Use known safe typeVersion values for each node type",
|
|
1064
|
+
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1065
|
+
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1066
|
+
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1067
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1068
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1069
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1070
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// src/telemetry/pattern-analyzer.ts
|
|
1074
|
+
var PATTERN_SCHEMA_VERSION = 2;
|
|
1075
|
+
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1076
|
+
telemetryDir;
|
|
1077
|
+
outputDir;
|
|
1078
|
+
_cachedEvents = null;
|
|
1079
|
+
constructor(telemetryDir) {
|
|
1080
|
+
const defaultDir = join4(homedir3(), ".kairos", "telemetry");
|
|
1081
|
+
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1082
|
+
this.outputDir = telemetryDir ? join4(telemetryDir, "..") : join4(homedir3(), ".kairos");
|
|
1083
|
+
}
|
|
1084
|
+
async loadPreviousPatterns() {
|
|
1146
1085
|
try {
|
|
1147
|
-
|
|
1086
|
+
const raw = await fsReadFile(join4(this.outputDir, "patterns.json"), "utf-8");
|
|
1087
|
+
const prev = JSON.parse(raw);
|
|
1088
|
+
const version = prev.schemaVersion ?? 0;
|
|
1089
|
+
const patterns = prev.topFailureRules ?? [];
|
|
1090
|
+
if (version === PATTERN_SCHEMA_VERSION) return patterns;
|
|
1091
|
+
return this.migratePatterns(patterns, version);
|
|
1148
1092
|
} catch {
|
|
1149
1093
|
return [];
|
|
1150
1094
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1095
|
+
}
|
|
1096
|
+
migratePatterns(patterns, fromVersion) {
|
|
1097
|
+
let migrated = patterns;
|
|
1098
|
+
if (fromVersion < 1) {
|
|
1099
|
+
migrated = migrated.map((p) => ({
|
|
1100
|
+
...p,
|
|
1101
|
+
compositeScore: p.compositeScore ?? 0,
|
|
1102
|
+
scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
|
|
1103
|
+
pipelineStage: p.pipelineStage ?? "node_generation"
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1106
|
+
if (fromVersion < 2) {
|
|
1107
|
+
migrated = migrated.map((p) => {
|
|
1108
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
1109
|
+
return {
|
|
1110
|
+
...p,
|
|
1111
|
+
scoringFactors: {
|
|
1112
|
+
...sf,
|
|
1113
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
1165
1114
|
}
|
|
1115
|
+
};
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
return migrated;
|
|
1119
|
+
}
|
|
1120
|
+
async analyze(days = 30) {
|
|
1121
|
+
const previousPatterns = await this.loadPreviousPatterns();
|
|
1122
|
+
const events = await this.readAllEvents(days);
|
|
1123
|
+
this._cachedEvents = events;
|
|
1124
|
+
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1125
|
+
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1126
|
+
const passed = attempts.filter(
|
|
1127
|
+
(a) => a.data.validationPassed === true
|
|
1128
|
+
);
|
|
1129
|
+
const failed = attempts.filter(
|
|
1130
|
+
(a) => a.data.validationPassed === false
|
|
1131
|
+
);
|
|
1132
|
+
const ruleFailures = /* @__PURE__ */ new Map();
|
|
1133
|
+
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1134
|
+
for (const a of failed) {
|
|
1135
|
+
const weight = this.recencyWeight(a.fileDate);
|
|
1136
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1137
|
+
const data = a.data;
|
|
1138
|
+
for (const issue of data.issues ?? []) {
|
|
1139
|
+
if (issue.severity === "warn") continue;
|
|
1140
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1141
|
+
entry.count++;
|
|
1142
|
+
entry.sessions.add(buildId);
|
|
1143
|
+
entry.recencyWeights.push(weight);
|
|
1144
|
+
entry.allMessages.push(issue.message);
|
|
1145
|
+
if (data.workflowType) {
|
|
1146
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
1147
|
+
}
|
|
1148
|
+
ruleFailures.set(issue.rule, entry);
|
|
1149
|
+
if (issue.rule === 17) {
|
|
1150
|
+
const credPatterns = [
|
|
1151
|
+
/credential\s+"([^"]+)"/,
|
|
1152
|
+
/credentialType[:\s]+"?([^"'\s]+)"?/,
|
|
1153
|
+
/missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
|
|
1154
|
+
];
|
|
1155
|
+
let credType = "unknown";
|
|
1156
|
+
for (const re of credPatterns) {
|
|
1157
|
+
const m = issue.message.match(re);
|
|
1158
|
+
if (m?.[1]) {
|
|
1159
|
+
credType = m[1];
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const failedByDate = /* @__PURE__ */ new Map();
|
|
1168
|
+
for (const a of failed) {
|
|
1169
|
+
failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
|
|
1170
|
+
}
|
|
1171
|
+
const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1172
|
+
const hasTrendData = sortedFailDates.length >= 3;
|
|
1173
|
+
let midDate = "";
|
|
1174
|
+
if (hasTrendData) {
|
|
1175
|
+
const halfTotal = failed.length / 2;
|
|
1176
|
+
let cumulative = 0;
|
|
1177
|
+
for (const [date, count] of sortedFailDates) {
|
|
1178
|
+
cumulative += count;
|
|
1179
|
+
if (cumulative >= halfTotal) {
|
|
1180
|
+
midDate = date;
|
|
1181
|
+
break;
|
|
1166
1182
|
}
|
|
1167
|
-
} catch {
|
|
1168
1183
|
}
|
|
1169
1184
|
}
|
|
1170
|
-
|
|
1185
|
+
const ruleTrends = /* @__PURE__ */ new Map();
|
|
1186
|
+
if (hasTrendData) {
|
|
1187
|
+
for (const a of failed) {
|
|
1188
|
+
const data = a.data;
|
|
1189
|
+
const isNewer = a.fileDate > midDate;
|
|
1190
|
+
for (const issue of data.issues ?? []) {
|
|
1191
|
+
const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
|
|
1192
|
+
if (isNewer) entry.newer++;
|
|
1193
|
+
else entry.older++;
|
|
1194
|
+
ruleTrends.set(issue.rule, entry);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1199
|
+
for (const a of attempts) {
|
|
1200
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1201
|
+
const list = sessions.get(buildId) ?? [];
|
|
1202
|
+
list.push(a);
|
|
1203
|
+
sessions.set(buildId, list);
|
|
1204
|
+
}
|
|
1205
|
+
let firstTryPass = 0;
|
|
1206
|
+
let correctionNeeded = 0;
|
|
1207
|
+
let singleAttemptFail = 0;
|
|
1208
|
+
for (const sessionAttempts of sessions.values()) {
|
|
1209
|
+
const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
|
|
1210
|
+
const lastPassed = lastAttempt.data.validationPassed === true;
|
|
1211
|
+
if (sessionAttempts.length === 1 && lastPassed) {
|
|
1212
|
+
firstTryPass++;
|
|
1213
|
+
} else if (sessionAttempts.length > 1 && lastPassed) {
|
|
1214
|
+
correctionNeeded++;
|
|
1215
|
+
} else {
|
|
1216
|
+
singleAttemptFail++;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
|
|
1220
|
+
const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
|
|
1221
|
+
const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
|
|
1222
|
+
const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
|
|
1223
|
+
const totalSessions = Math.max(sessions.size, 1);
|
|
1224
|
+
const stickinessCount = /* @__PURE__ */ new Map();
|
|
1225
|
+
for (const sessionAttempts of sessions.values()) {
|
|
1226
|
+
if (sessionAttempts.length < 2) continue;
|
|
1227
|
+
for (let i = 0; i < sessionAttempts.length - 1; i++) {
|
|
1228
|
+
const curr = sessionAttempts[i].data;
|
|
1229
|
+
const next = sessionAttempts[i + 1].data;
|
|
1230
|
+
if (curr.validationPassed !== false || next.validationPassed !== false) continue;
|
|
1231
|
+
const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
|
|
1232
|
+
const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
|
|
1233
|
+
for (const rule of currRules) {
|
|
1234
|
+
if (nextRules.has(rule)) {
|
|
1235
|
+
stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const CONFIRMED_THRESHOLD = 3;
|
|
1241
|
+
const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
|
|
1242
|
+
const RESOLVED_TTL_DAYS = 90;
|
|
1243
|
+
const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
|
|
1244
|
+
const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
|
|
1245
|
+
const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
|
|
1246
|
+
const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
|
|
1247
|
+
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
1248
|
+
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
1249
|
+
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
1250
|
+
const pattern = {
|
|
1251
|
+
rule,
|
|
1252
|
+
failureCount: entry.count,
|
|
1253
|
+
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
1254
|
+
compositeScore,
|
|
1255
|
+
scoringFactors: factors,
|
|
1256
|
+
state,
|
|
1257
|
+
trend: this.classifyTrend(t.older, t.newer),
|
|
1258
|
+
pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
|
|
1259
|
+
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
1260
|
+
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
1261
|
+
};
|
|
1262
|
+
if (entry.workflowTypes.size > 0) {
|
|
1263
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
1264
|
+
}
|
|
1265
|
+
return pattern;
|
|
1266
|
+
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1267
|
+
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
1268
|
+
for (const p of activePatterns) {
|
|
1269
|
+
const prev = previousPatterns.find((pp) => pp.rule === p.rule);
|
|
1270
|
+
if (prev?.state === "resolved") {
|
|
1271
|
+
p.trend = "worsening";
|
|
1272
|
+
p.regressed = true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const ruleLastFailureDate = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const a of failed) {
|
|
1277
|
+
const data = a.data;
|
|
1278
|
+
for (const issue of data.issues ?? []) {
|
|
1279
|
+
const existing = ruleLastFailureDate.get(issue.rule);
|
|
1280
|
+
if (!existing || a.fileDate > existing) {
|
|
1281
|
+
ruleLastFailureDate.set(issue.rule, a.fileDate);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const newlyResolved = previousPatterns.filter((p) => {
|
|
1286
|
+
if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
|
|
1287
|
+
const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
|
|
1288
|
+
const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
|
|
1289
|
+
return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
|
|
1290
|
+
}).map((p) => ({
|
|
1291
|
+
...p,
|
|
1292
|
+
state: "resolved",
|
|
1293
|
+
trend: "improving",
|
|
1294
|
+
pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
|
|
1295
|
+
confidence: 0,
|
|
1296
|
+
compositeScore: 0,
|
|
1297
|
+
scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
|
|
1298
|
+
failureCount: 0,
|
|
1299
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1300
|
+
}));
|
|
1301
|
+
const ttlCutoff = /* @__PURE__ */ new Date();
|
|
1302
|
+
ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
|
|
1303
|
+
const ttlCutoffStr = ttlCutoff.toISOString();
|
|
1304
|
+
const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
|
|
1305
|
+
const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
|
|
1306
|
+
const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
|
|
1307
|
+
const deduped = [
|
|
1308
|
+
...newlyResolved,
|
|
1309
|
+
...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
|
|
1310
|
+
...pendingResolution
|
|
1311
|
+
];
|
|
1312
|
+
const patterns = [...activePatterns, ...deduped];
|
|
1313
|
+
const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
|
|
1314
|
+
const drift = this.detectDrift(patterns);
|
|
1315
|
+
const warnEffMap = /* @__PURE__ */ new Map();
|
|
1316
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
1317
|
+
for (const bc of buildCompletes) {
|
|
1318
|
+
const bcData = bc.data;
|
|
1319
|
+
const warned = bcData.warnedRules ?? [];
|
|
1320
|
+
if (warned.length === 0) continue;
|
|
1321
|
+
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
1322
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1323
|
+
for (const a of sessionAttempts) {
|
|
1324
|
+
const ad = a.data;
|
|
1325
|
+
if (ad.validationPassed === false) {
|
|
1326
|
+
for (const issue of ad.issues ?? []) {
|
|
1327
|
+
sessionFailedRules.add(issue.rule);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
for (const rule of warned) {
|
|
1332
|
+
const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
|
|
1333
|
+
entry.warned++;
|
|
1334
|
+
if (sessionFailedRules.has(rule)) entry.failed++;
|
|
1335
|
+
else entry.passed++;
|
|
1336
|
+
warnEffMap.set(rule, entry);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
|
|
1340
|
+
rule,
|
|
1341
|
+
timesWarned: e.warned,
|
|
1342
|
+
timesWarnedAndPassed: e.passed,
|
|
1343
|
+
timesWarnedAndFailed: e.failed,
|
|
1344
|
+
effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
|
|
1345
|
+
})).sort((a, b) => b.timesWarned - a.timesWarned);
|
|
1346
|
+
const coOccurrenceMap = /* @__PURE__ */ new Map();
|
|
1347
|
+
for (const a of failed) {
|
|
1348
|
+
const data = a.data;
|
|
1349
|
+
const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
|
|
1350
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1351
|
+
for (let j = i + 1; j < rules.length; j++) {
|
|
1352
|
+
const key = `${rules[i]},${rules[j]}`;
|
|
1353
|
+
coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
|
|
1358
|
+
const [a, b] = key.split(",").map(Number);
|
|
1359
|
+
return { rules: [a, b], count };
|
|
1360
|
+
}).sort((a, b) => b.count - a.count);
|
|
1361
|
+
const attemptDistribution = {};
|
|
1362
|
+
for (const sessionAttempts of sessions.values()) {
|
|
1363
|
+
const depth = sessionAttempts.length;
|
|
1364
|
+
attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
schemaVersion: PATTERN_SCHEMA_VERSION,
|
|
1368
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1369
|
+
summary: {
|
|
1370
|
+
totalBuilds: starts.length,
|
|
1371
|
+
totalAttempts: attempts.length,
|
|
1372
|
+
firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
|
|
1373
|
+
correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
|
|
1374
|
+
singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
|
|
1375
|
+
avgDurationMs: Math.round(avgDuration),
|
|
1376
|
+
totalTokensInput: totalInput,
|
|
1377
|
+
totalTokensOutput: totalOutput,
|
|
1378
|
+
attemptDistribution
|
|
1379
|
+
},
|
|
1380
|
+
topFailureRules: patterns,
|
|
1381
|
+
failingCredentialTypes: credTypes,
|
|
1382
|
+
drift,
|
|
1383
|
+
warningEffectiveness,
|
|
1384
|
+
ruleCoOccurrence
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
async analyzeAndSave(days = 30) {
|
|
1388
|
+
const analysis = await this.analyze(days);
|
|
1389
|
+
await mkdir2(this.outputDir, { recursive: true });
|
|
1390
|
+
const outputPath = join4(this.outputDir, "patterns.json");
|
|
1391
|
+
const tmpPath = `${outputPath}.tmp`;
|
|
1392
|
+
await writeFile(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
1393
|
+
await rename(tmpPath, outputPath);
|
|
1394
|
+
const historySummary = {
|
|
1395
|
+
timestamp: analysis.generatedAt,
|
|
1396
|
+
totalBuilds: analysis.summary.totalBuilds,
|
|
1397
|
+
firstTryPassRate: analysis.summary.firstTryPassRate,
|
|
1398
|
+
correctionRate: analysis.summary.correctionRate,
|
|
1399
|
+
singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
|
|
1400
|
+
activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
|
|
1401
|
+
topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
|
|
1402
|
+
};
|
|
1403
|
+
const historyPath = join4(this.outputDir, "pattern-history.jsonl");
|
|
1404
|
+
await appendFile2(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
1405
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
1406
|
+
const sessionHistoryPath = join4(this.outputDir, "session-history.json");
|
|
1407
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
1408
|
+
await writeFile(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
1409
|
+
await rename(sessionHistoryTmp, sessionHistoryPath);
|
|
1410
|
+
return analysis;
|
|
1411
|
+
}
|
|
1412
|
+
async getSessions(limit = 20) {
|
|
1413
|
+
try {
|
|
1414
|
+
const raw = await fsReadFile(join4(this.outputDir, "session-history.json"), "utf-8");
|
|
1415
|
+
const all = JSON.parse(raw);
|
|
1416
|
+
return all.slice(-limit);
|
|
1417
|
+
} catch {
|
|
1418
|
+
return [];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async buildSessionSummaries(days = 30) {
|
|
1422
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
1423
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
1424
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
1425
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
1426
|
+
const buildId = e.runId ?? e.sessionId;
|
|
1427
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
1428
|
+
list.push(e);
|
|
1429
|
+
attemptsByBuild.set(buildId, list);
|
|
1430
|
+
}
|
|
1431
|
+
const summaries = buildCompletes.map((bc) => {
|
|
1432
|
+
const data = bc.data;
|
|
1433
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1434
|
+
const failedRules = Array.from(new Set(
|
|
1435
|
+
sessionAttempts.flatMap((a) => {
|
|
1436
|
+
const ad = a.data;
|
|
1437
|
+
if (ad.validationPassed !== false) return [];
|
|
1438
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
1439
|
+
})
|
|
1440
|
+
));
|
|
1441
|
+
return {
|
|
1442
|
+
sessionId: bc.sessionId,
|
|
1443
|
+
date: bc.fileDate,
|
|
1444
|
+
description: data.description ?? "",
|
|
1445
|
+
workflowType: data.workflowType ?? null,
|
|
1446
|
+
attempts: data.totalAttempts ?? 1,
|
|
1447
|
+
success: data.success ?? false,
|
|
1448
|
+
failedRules,
|
|
1449
|
+
workflowName: data.workflowName ?? null
|
|
1450
|
+
};
|
|
1451
|
+
});
|
|
1452
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
1453
|
+
}
|
|
1454
|
+
async getHistory(limit = 20) {
|
|
1455
|
+
try {
|
|
1456
|
+
const raw = await fsReadFile(join4(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
1457
|
+
return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
|
|
1458
|
+
} catch {
|
|
1459
|
+
return [];
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
static fromEnv() {
|
|
1463
|
+
const dir = process.env["KAIROS_TELEMETRY"];
|
|
1464
|
+
return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
|
|
1465
|
+
}
|
|
1466
|
+
detectDrift(patterns) {
|
|
1467
|
+
const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
|
|
1468
|
+
const validatorRuleSet = new Set(VALIDATOR_RULES);
|
|
1469
|
+
const alerts = [];
|
|
1470
|
+
for (const p of patterns) {
|
|
1471
|
+
if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
|
|
1472
|
+
alerts.push({
|
|
1473
|
+
type: "stale_pattern",
|
|
1474
|
+
rule: p.rule,
|
|
1475
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
for (const rule of VALIDATOR_RULES) {
|
|
1480
|
+
if (!(rule in RULE_MITIGATIONS)) {
|
|
1481
|
+
alerts.push({
|
|
1482
|
+
type: "missing_mitigation",
|
|
1483
|
+
rule,
|
|
1484
|
+
message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
if (!(rule in RULE_PIPELINE_STAGES)) {
|
|
1488
|
+
alerts.push({
|
|
1489
|
+
type: "missing_stage_mapping",
|
|
1490
|
+
rule,
|
|
1491
|
+
message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
|
|
1496
|
+
return {
|
|
1497
|
+
healthy: alerts.length === 0,
|
|
1498
|
+
alerts,
|
|
1499
|
+
coveredRules,
|
|
1500
|
+
totalRules: VALIDATOR_RULES.length
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
|
|
1504
|
+
const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
|
|
1505
|
+
const stateWeight = stateWeights[state];
|
|
1506
|
+
const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
|
|
1507
|
+
const stickinessBoost = Math.min(0.15, stickiness * 0.05);
|
|
1508
|
+
const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
|
|
1509
|
+
return {
|
|
1510
|
+
compositeScore,
|
|
1511
|
+
factors: {
|
|
1512
|
+
rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
1513
|
+
impact: Math.round(impact * 1e3) / 1e3,
|
|
1514
|
+
recency: Math.round(avgRecency * 1e3) / 1e3,
|
|
1515
|
+
stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
classifyTrend(older, newer) {
|
|
1520
|
+
const total = older + newer;
|
|
1521
|
+
if (total === 0) return "stable";
|
|
1522
|
+
if (older === 0) return "new";
|
|
1523
|
+
const newerRatio = newer / total;
|
|
1524
|
+
if (newerRatio >= 0.65) return "worsening";
|
|
1525
|
+
if (newerRatio <= 0.35) return "improving";
|
|
1526
|
+
return "stable";
|
|
1527
|
+
}
|
|
1528
|
+
deduplicateMessages(messages, maxCount = 3) {
|
|
1529
|
+
const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
|
|
1530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1531
|
+
const unique = [];
|
|
1532
|
+
for (const msg of messages) {
|
|
1533
|
+
const key = normalize(msg);
|
|
1534
|
+
if (!seen.has(key) && unique.length < maxCount) {
|
|
1535
|
+
seen.add(key);
|
|
1536
|
+
unique.push(msg);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return unique;
|
|
1540
|
+
}
|
|
1541
|
+
recencyWeight(fileDate, halfLifeDays = 30) {
|
|
1542
|
+
const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
|
|
1543
|
+
return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
|
|
1544
|
+
}
|
|
1545
|
+
async readAllEvents(days) {
|
|
1546
|
+
return readTelemetryEvents(this.telemetryDir, days);
|
|
1171
1547
|
}
|
|
1172
1548
|
};
|
|
1173
1549
|
|
|
@@ -1399,13 +1775,17 @@ function rerank(candidates, clusters) {
|
|
|
1399
1775
|
}
|
|
1400
1776
|
|
|
1401
1777
|
// src/library/file-library.ts
|
|
1402
|
-
import { readFile as
|
|
1403
|
-
import { join as
|
|
1404
|
-
import { homedir as
|
|
1778
|
+
import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir3, stat } from "fs/promises";
|
|
1779
|
+
import { join as join5 } from "path";
|
|
1780
|
+
import { homedir as homedir4 } from "os";
|
|
1405
1781
|
|
|
1406
|
-
// src/utils/
|
|
1407
|
-
|
|
1408
|
-
|
|
1782
|
+
// src/utils/thresholds.ts
|
|
1783
|
+
var DIRECT_THRESHOLD = 0.92;
|
|
1784
|
+
var REFERENCE_THRESHOLD = 0.72;
|
|
1785
|
+
function scoreToMode(score) {
|
|
1786
|
+
if (score >= DIRECT_THRESHOLD) return "direct";
|
|
1787
|
+
if (score >= REFERENCE_THRESHOLD) return "reference";
|
|
1788
|
+
return "scratch";
|
|
1409
1789
|
}
|
|
1410
1790
|
|
|
1411
1791
|
// src/library/file-library.ts
|
|
@@ -1421,13 +1801,27 @@ function buildSearchCorpus(w) {
|
|
|
1421
1801
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
1422
1802
|
}
|
|
1423
1803
|
var MAX_LIBRARY_SIZE = 500;
|
|
1804
|
+
function isValidMeta(item) {
|
|
1805
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
1806
|
+
}
|
|
1807
|
+
function isValidOldEntry(item) {
|
|
1808
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
1809
|
+
item.workflow.nodes
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1424
1812
|
var FileLibrary = class {
|
|
1425
1813
|
dir;
|
|
1426
|
-
|
|
1814
|
+
meta = [];
|
|
1427
1815
|
initPromise = null;
|
|
1428
1816
|
writeQueue = Promise.resolve();
|
|
1429
1817
|
constructor(dir) {
|
|
1430
|
-
this.dir = dir ??
|
|
1818
|
+
this.dir = dir ?? join5(homedir4(), ".kairos", "library");
|
|
1819
|
+
}
|
|
1820
|
+
get workflowsDir() {
|
|
1821
|
+
return join5(this.dir, "workflows");
|
|
1822
|
+
}
|
|
1823
|
+
workflowFilePath(id) {
|
|
1824
|
+
return join5(this.workflowsDir, `${id}.json`);
|
|
1431
1825
|
}
|
|
1432
1826
|
async initialize() {
|
|
1433
1827
|
if (!this.initPromise) {
|
|
@@ -1436,62 +1830,149 @@ var FileLibrary = class {
|
|
|
1436
1830
|
return this.initPromise;
|
|
1437
1831
|
}
|
|
1438
1832
|
async doInitialize() {
|
|
1439
|
-
await
|
|
1440
|
-
const indexPath =
|
|
1833
|
+
await mkdir3(this.dir, { recursive: true });
|
|
1834
|
+
const indexPath = join5(this.dir, "index.json");
|
|
1835
|
+
let workflowsDirExists = false;
|
|
1441
1836
|
try {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
);
|
|
1837
|
+
await stat(this.workflowsDir);
|
|
1838
|
+
workflowsDirExists = true;
|
|
1839
|
+
} catch {
|
|
1840
|
+
}
|
|
1841
|
+
if (workflowsDirExists) {
|
|
1842
|
+
try {
|
|
1843
|
+
const raw = await readFile(indexPath, "utf-8");
|
|
1844
|
+
const parsed = JSON.parse(raw);
|
|
1845
|
+
if (Array.isArray(parsed)) {
|
|
1846
|
+
this.meta = parsed.filter(isValidMeta);
|
|
1847
|
+
}
|
|
1848
|
+
} catch {
|
|
1849
|
+
this.meta = [];
|
|
1450
1850
|
}
|
|
1851
|
+
} else {
|
|
1852
|
+
try {
|
|
1853
|
+
const raw = await readFile(indexPath, "utf-8");
|
|
1854
|
+
const parsed = JSON.parse(raw);
|
|
1855
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
1856
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
this.meta = [];
|
|
1862
|
+
await mkdir3(this.workflowsDir, { recursive: true });
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
1867
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
1868
|
+
* meta entry. Rewrites index.json in the new format.
|
|
1869
|
+
*/
|
|
1870
|
+
async migrateFromMonolithic(oldEntries) {
|
|
1871
|
+
await mkdir3(this.workflowsDir, { recursive: true });
|
|
1872
|
+
const newMeta = [];
|
|
1873
|
+
for (const entry of oldEntries) {
|
|
1874
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
1875
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
1876
|
+
await writeFile2(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
1877
|
+
await rename2(tmpPath, wfPath);
|
|
1878
|
+
const { workflow, ...metaFields } = entry;
|
|
1879
|
+
newMeta.push({
|
|
1880
|
+
...metaFields,
|
|
1881
|
+
workflowName: workflow.name,
|
|
1882
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
this.meta = newMeta;
|
|
1886
|
+
await this.persistNow();
|
|
1887
|
+
}
|
|
1888
|
+
async loadWorkflowFile(id) {
|
|
1889
|
+
try {
|
|
1890
|
+
const raw = await readFile(this.workflowFilePath(id), "utf-8");
|
|
1891
|
+
return JSON.parse(raw);
|
|
1451
1892
|
} catch {
|
|
1452
|
-
|
|
1893
|
+
return null;
|
|
1453
1894
|
}
|
|
1454
1895
|
}
|
|
1896
|
+
async writeWorkflowFile(id, workflow) {
|
|
1897
|
+
const wfPath = this.workflowFilePath(id);
|
|
1898
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
1899
|
+
await writeFile2(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
1900
|
+
await rename2(tmpPath, wfPath);
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
1904
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
1905
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
1906
|
+
*/
|
|
1907
|
+
makeSearchShell(m) {
|
|
1908
|
+
return {
|
|
1909
|
+
...m,
|
|
1910
|
+
workflow: {
|
|
1911
|
+
name: m.workflowName,
|
|
1912
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
1913
|
+
id: "",
|
|
1914
|
+
name: "",
|
|
1915
|
+
type,
|
|
1916
|
+
typeVersion: 1,
|
|
1917
|
+
position: [0, 0],
|
|
1918
|
+
parameters: {}
|
|
1919
|
+
})),
|
|
1920
|
+
connections: {}
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1455
1924
|
async search(description, options) {
|
|
1456
|
-
const
|
|
1457
|
-
if (
|
|
1925
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
1926
|
+
if (filteredMeta.length === 0) return [];
|
|
1458
1927
|
const limit = options?.limit ?? 3;
|
|
1459
1928
|
const queryTokens = tokenize(description);
|
|
1460
1929
|
if (queryTokens.length === 0) return [];
|
|
1461
|
-
const
|
|
1930
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
1931
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
1462
1932
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
1463
|
-
const docCount =
|
|
1933
|
+
const docCount = shells.length;
|
|
1464
1934
|
const idf = /* @__PURE__ */ new Map();
|
|
1465
1935
|
const allTokens = new Set(queryTokens);
|
|
1466
1936
|
for (const token of allTokens) {
|
|
1467
1937
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
1468
1938
|
idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
|
|
1469
1939
|
}
|
|
1470
|
-
const scored = hybridScore(queryTokens, description,
|
|
1471
|
-
const clusters = clusterWorkflows(
|
|
1940
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
1941
|
+
const clusters = clusterWorkflows(shells);
|
|
1472
1942
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1943
|
+
if (reranked.length === 0) return [];
|
|
1944
|
+
for (const r of reranked) {
|
|
1945
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
1946
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
1947
|
+
}
|
|
1948
|
+
this.persist();
|
|
1949
|
+
const results = await Promise.all(
|
|
1950
|
+
reranked.map(async (r) => {
|
|
1951
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
1952
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
1953
|
+
if (!workflow) return null;
|
|
1954
|
+
return {
|
|
1955
|
+
workflow: { ...m, workflow },
|
|
1956
|
+
score: r.score,
|
|
1957
|
+
mode: scoreToMode(r.score)
|
|
1958
|
+
};
|
|
1959
|
+
})
|
|
1960
|
+
);
|
|
1961
|
+
return results.filter((r) => r !== null);
|
|
1483
1962
|
}
|
|
1484
1963
|
async save(workflow, metadata) {
|
|
1485
1964
|
const id = generateUUID();
|
|
1965
|
+
await this.writeWorkflowFile(id, workflow);
|
|
1486
1966
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
1487
|
-
const
|
|
1967
|
+
const meta = {
|
|
1488
1968
|
id,
|
|
1489
|
-
workflow,
|
|
1490
1969
|
description: metadata.description,
|
|
1491
1970
|
tags: metadata.tags ?? [],
|
|
1492
1971
|
platform: metadata.platform ?? "n8n",
|
|
1493
1972
|
deployCount: 0,
|
|
1494
1973
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1974
|
+
workflowName: workflow.name,
|
|
1975
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
1495
1976
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
1496
1977
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
1497
1978
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -1503,31 +1984,35 @@ var FileLibrary = class {
|
|
|
1503
1984
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
1504
1985
|
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
1505
1986
|
};
|
|
1506
|
-
this.
|
|
1507
|
-
if (this.
|
|
1508
|
-
this.
|
|
1509
|
-
|
|
1987
|
+
this.meta.push(meta);
|
|
1988
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
1989
|
+
this.meta.sort((a, b) => {
|
|
1990
|
+
if (a.id === id) return -1;
|
|
1991
|
+
if (b.id === id) return 1;
|
|
1992
|
+
return (b.deployCount ?? 0) - (a.deployCount ?? 0);
|
|
1993
|
+
});
|
|
1994
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
1510
1995
|
}
|
|
1511
1996
|
await this.persist();
|
|
1512
1997
|
return id;
|
|
1513
1998
|
}
|
|
1514
1999
|
async recordDeployment(id) {
|
|
1515
|
-
const
|
|
1516
|
-
if (
|
|
1517
|
-
|
|
1518
|
-
|
|
2000
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
2001
|
+
if (m) {
|
|
2002
|
+
m.deployCount++;
|
|
2003
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1519
2004
|
await this.persist();
|
|
1520
2005
|
}
|
|
1521
2006
|
}
|
|
1522
2007
|
async recordOutcome(id, outcome) {
|
|
1523
|
-
const
|
|
1524
|
-
if (!
|
|
2008
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
2009
|
+
if (!m) return;
|
|
1525
2010
|
if (outcome.mode === "direct") {
|
|
1526
|
-
|
|
2011
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
1527
2012
|
} else {
|
|
1528
|
-
|
|
2013
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
1529
2014
|
}
|
|
1530
|
-
const stats =
|
|
2015
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
1531
2016
|
stats.totalUses++;
|
|
1532
2017
|
stats.totalAttempts += outcome.attempts;
|
|
1533
2018
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -1535,24 +2020,35 @@ var FileLibrary = class {
|
|
|
1535
2020
|
const key = String(rule);
|
|
1536
2021
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
1537
2022
|
}
|
|
1538
|
-
|
|
2023
|
+
m.outcomeStats = stats;
|
|
1539
2024
|
await this.persist();
|
|
1540
2025
|
}
|
|
1541
2026
|
async drain() {
|
|
1542
2027
|
await this.writeQueue;
|
|
1543
2028
|
}
|
|
1544
2029
|
async get(id) {
|
|
1545
|
-
|
|
2030
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
2031
|
+
if (!m) return null;
|
|
2032
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
2033
|
+
if (!workflow) return null;
|
|
2034
|
+
return { ...m, workflow };
|
|
1546
2035
|
}
|
|
1547
2036
|
async list(filters) {
|
|
1548
|
-
let
|
|
2037
|
+
let filtered = this.meta;
|
|
1549
2038
|
if (filters?.platform) {
|
|
1550
|
-
|
|
2039
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
1551
2040
|
}
|
|
1552
2041
|
if (filters?.tags && filters.tags.length > 0) {
|
|
1553
|
-
|
|
1554
|
-
}
|
|
1555
|
-
|
|
2042
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2043
|
+
}
|
|
2044
|
+
const results = await Promise.all(
|
|
2045
|
+
filtered.map(async (m) => {
|
|
2046
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
2047
|
+
if (!workflow) return null;
|
|
2048
|
+
return { ...m, workflow };
|
|
2049
|
+
})
|
|
2050
|
+
);
|
|
2051
|
+
return results.filter((r) => r !== null);
|
|
1556
2052
|
}
|
|
1557
2053
|
deduplicateFailurePatterns(patterns) {
|
|
1558
2054
|
if (!patterns?.length) return void 0;
|
|
@@ -1567,12 +2063,37 @@ var FileLibrary = class {
|
|
|
1567
2063
|
}
|
|
1568
2064
|
return [...map.values()];
|
|
1569
2065
|
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
2068
|
+
*/
|
|
2069
|
+
async persistNow() {
|
|
2070
|
+
const indexPath = join5(this.dir, "index.json");
|
|
2071
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
2072
|
+
await writeFile2(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
2073
|
+
await rename2(tmpPath, indexPath);
|
|
2074
|
+
}
|
|
1570
2075
|
persist() {
|
|
1571
2076
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
1572
|
-
const indexPath =
|
|
2077
|
+
const indexPath = join5(this.dir, "index.json");
|
|
2078
|
+
let onDisk = [];
|
|
2079
|
+
try {
|
|
2080
|
+
const raw = await readFile(indexPath, "utf-8");
|
|
2081
|
+
const parsed = JSON.parse(raw);
|
|
2082
|
+
if (Array.isArray(parsed)) {
|
|
2083
|
+
onDisk = parsed.filter(isValidMeta);
|
|
2084
|
+
}
|
|
2085
|
+
} catch {
|
|
2086
|
+
}
|
|
2087
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
2088
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
2089
|
+
let merged = [...this.meta, ...external];
|
|
2090
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
2091
|
+
merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
|
|
2092
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
2093
|
+
}
|
|
1573
2094
|
const tmpPath = `${indexPath}.tmp`;
|
|
1574
|
-
await
|
|
1575
|
-
await
|
|
2095
|
+
await writeFile2(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2096
|
+
await rename2(tmpPath, indexPath);
|
|
1576
2097
|
});
|
|
1577
2098
|
return this.writeQueue;
|
|
1578
2099
|
}
|
|
@@ -1589,8 +2110,11 @@ export {
|
|
|
1589
2110
|
NodeRegistry,
|
|
1590
2111
|
N8nValidator,
|
|
1591
2112
|
scoreToMode,
|
|
1592
|
-
|
|
2113
|
+
RULE_EXAMPLES,
|
|
2114
|
+
RULE_MITIGATIONS,
|
|
2115
|
+
TelemetryCollector,
|
|
1593
2116
|
TelemetryReader,
|
|
2117
|
+
PatternAnalyzer,
|
|
1594
2118
|
nullLogger,
|
|
1595
2119
|
hybridScore,
|
|
1596
2120
|
clusterWorkflows,
|
|
@@ -1599,4 +2123,4 @@ export {
|
|
|
1599
2123
|
buildSearchCorpus,
|
|
1600
2124
|
FileLibrary
|
|
1601
2125
|
};
|
|
1602
|
-
//# sourceMappingURL=chunk-
|
|
2126
|
+
//# sourceMappingURL=chunk-6IXW3WCC.js.map
|