@replikanti/flowlint-core 0.9.2 → 0.9.4

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 CHANGED
@@ -1,75 +1,85 @@
1
- # @replikanti/flowlint-core
2
-
3
- Core linting engine for n8n workflows. This package provides the fundamental building blocks for analyzing and validating n8n workflow files.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @replikanti/flowlint-core
9
- ```
10
-
11
- ## Usage
12
-
13
- ```typescript
14
- import { parseN8n, runAllRules, loadConfig, defaultConfig } from '@replikanti/flowlint-core';
15
-
16
- // Parse a workflow from JSON string
17
- const workflow = parseN8n(workflowJsonString);
18
-
19
- // Run all linting rules
20
- const findings = runAllRules(workflow, {
21
- path: 'my-workflow.n8n.json',
22
- cfg: defaultConfig,
23
- });
24
-
25
- // Process findings
26
- findings.forEach(finding => {
27
- console.log([${finding.severity.toUpperCase()}] ${finding.rule}: ${finding.message});
28
- });
29
- ```
30
-
31
- ## API
32
-
33
- ### Parser
34
-
35
- - `parseN8n(doc: string): Graph` - Parse n8n workflow JSON/YAML into a graph structure
36
-
37
- ### Linting
38
-
39
- - `runAllRules(graph: Graph, ctx: RuleContext): Finding[]` - Run all enabled rules
40
-
41
- ### Configuration
42
-
43
- - `loadConfig(configPath?: string): FlowLintConfig` - Load configuration from file
44
- - `defaultConfig: FlowLintConfig` - Default configuration
45
- - `parseConfig(content: string): FlowLintConfig` - Parse config from YAML string
46
-
47
- ### Validation
48
-
49
- - `validateN8nWorkflow(data: unknown): void` - Validate workflow structure
50
-
51
- ## Rules
52
-
53
- This package includes 14 built-in rules:
54
-
55
- | Rule | Description | Severity |
56
- |------|-------------|----------|
57
- | R1 | Rate limit retry | must |
58
- | R2 | Error handling | must |
59
- | R3 | Idempotency | should |
60
- | R4 | Secrets exposure | must |
61
- | R5 | Dead ends | nit |
62
- | R6 | Long running | should |
63
- | R7 | Alert/log enforcement | should |
64
- | R8 | Unused data | nit |
65
- | R9 | Config literals | should |
66
- | R10 | Naming convention | nit |
67
- | R11 | Deprecated nodes | should |
68
- | R12 | Unhandled error path | must |
69
- | R13 | Webhook acknowledgment | must |
70
- | R14 | Retry-After compliance | should |
71
-
72
- ## License
73
-
1
+ # @replikanti/flowlint-core
2
+
3
+ ![Coverage](https://img.shields.io/badge/coverage-90%25-green)
4
+
5
+ Core linting engine for n8n workflows. This package provides the fundamental building blocks for analyzing and validating n8n workflow files.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @replikanti/flowlint-core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { parseN8n, runAllRules, loadConfig, defaultConfig } from '@replikanti/flowlint-core';
17
+
18
+ // Parse a workflow from JSON string
19
+ const workflow = parseN8n(workflowJsonString);
20
+
21
+ // Run all linting rules
22
+ const findings = runAllRules(workflow, {
23
+ path: 'my-workflow.n8n.json',
24
+ cfg: defaultConfig,
25
+ });
26
+
27
+ // Process findings
28
+ findings.forEach(finding => {
29
+ console.log(`[\${finding.severity.toUpperCase()}] \${finding.rule}: \${finding.message}`);
30
+ });
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### Parser
36
+
37
+ - `parseN8n(doc: string): Graph` - Parse n8n workflow JSON/YAML into a graph structure
38
+
39
+ ### Linting
40
+
41
+ - `runAllRules(graph: Graph, ctx: RuleContext): Finding[]` - Run all enabled rules
42
+
43
+ ### Configuration
44
+
45
+ - `loadConfig(configPath?: string): FlowLintConfig` - Load configuration from file
46
+ - `defaultConfig: FlowLintConfig` - Default configuration
47
+ - `parseConfig(content: string): FlowLintConfig` - Parse config from YAML string
48
+
49
+ ### Validation
50
+
51
+ - `validateN8nWorkflow(data: unknown): void` - Validate workflow structure
52
+
53
+ ## Testing
54
+
55
+ Run tests with coverage reporting:
56
+
57
+ ```bash
58
+ npm run test:coverage
59
+ ```
60
+
61
+ ## Rules
62
+
63
+ This package includes 14 built-in rules:
64
+
65
+ | Rule | Description | Severity |
66
+ |------|-------------|----------|
67
+ | R1 | Rate limit retry | must |
68
+ | R2 | Error handling | must |
69
+ | R3 | Idempotency | should |
70
+ | R4 | Secrets exposure | must |
71
+ | R5 | Dead ends | nit |
72
+ | R6 | Long running | should |
73
+ | R7 | Alert/log enforcement | should |
74
+ | R8 | Unused data | nit |
75
+ | R9 | Config literals | should |
76
+ | R10 | Naming convention | nit |
77
+ | R11 | Deprecated nodes | should |
78
+ | R12 | Unhandled error path | must |
79
+ | R13 | Webhook acknowledgment | must |
80
+ | R14 | Retry-After compliance | should |
81
+
82
+ ## License
83
+
74
84
  MIT
75
85
 
package/dist/index.js CHANGED
@@ -883,6 +883,30 @@ var metadata7 = {
883
883
  description: "Ensures critical paths include logging or alerting steps.",
884
884
  details: "For example, a failed payment processing branch should trigger an alert for monitoring."
885
885
  };
886
+ function isPathHandled(graph, startNodeId) {
887
+ const queue = [startNodeId];
888
+ const visited = /* @__PURE__ */ new Set([startNodeId]);
889
+ let head = 0;
890
+ while (head < queue.length) {
891
+ const currentId = queue[head++];
892
+ const currentNode = graph.nodes.find((n) => n.id === currentId);
893
+ if (!currentNode) continue;
894
+ if (isNotificationNode(currentNode.type) || isErrorHandlerNode(currentNode.type, currentNode.name)) {
895
+ return true;
896
+ }
897
+ if (isRejoinNode(graph, currentId)) {
898
+ continue;
899
+ }
900
+ const outgoing = graph.edges.filter((e) => e.from === currentId);
901
+ for (const outEdge of outgoing) {
902
+ if (!visited.has(outEdge.to)) {
903
+ visited.add(outEdge.to);
904
+ queue.push(outEdge.to);
905
+ }
906
+ }
907
+ }
908
+ return false;
909
+ }
886
910
  function r7AlertLogEnforcement(graph, ctx) {
887
911
  const cfg = ctx.cfg.rules.alert_log_enforcement;
888
912
  if (!cfg?.enabled) return [];
@@ -890,29 +914,7 @@ function r7AlertLogEnforcement(graph, ctx) {
890
914
  const errorEdges = graph.edges.filter((edge) => edge.on === "error");
891
915
  for (const edge of errorEdges) {
892
916
  const fromNode = graph.nodes.find((n) => n.id === edge.from);
893
- let isHandled = false;
894
- const queue = [edge.to];
895
- const visited = /* @__PURE__ */ new Set([edge.to]);
896
- let head = 0;
897
- while (head < queue.length) {
898
- const currentId = queue[head++];
899
- const currentNode = graph.nodes.find((n) => n.id === currentId);
900
- if (isNotificationNode(currentNode.type) || isErrorHandlerNode(currentNode.type, currentNode.name)) {
901
- isHandled = true;
902
- break;
903
- }
904
- if (isRejoinNode(graph, currentId)) {
905
- continue;
906
- }
907
- const outgoing = graph.edges.filter((e) => e.from === currentId);
908
- for (const outEdge of outgoing) {
909
- if (!visited.has(outEdge.to)) {
910
- visited.add(outEdge.to);
911
- queue.push(outEdge.to);
912
- }
913
- }
914
- }
915
- if (!isHandled) {
917
+ if (!isPathHandled(graph, edge.to)) {
916
918
  findings.push({
917
919
  rule: metadata7.id,
918
920
  severity: metadata7.severity,
@@ -1137,7 +1139,7 @@ var r14RetryAfterCompliance = createNodeRule(metadata14.id, metadata14.name, (no
1137
1139
  const waitBetweenTries = node.flags?.waitBetweenTries;
1138
1140
  if (waitBetweenTries !== void 0 && waitBetweenTries !== null) {
1139
1141
  if (typeof waitBetweenTries === "number") return null;
1140
- if (typeof waitBetweenTries === "string" && !isNaN(Number(waitBetweenTries)) && !waitBetweenTries.includes("{{")) {
1142
+ if (typeof waitBetweenTries === "string" && !Number.isNaN(Number(waitBetweenTries)) && !waitBetweenTries.includes("{{")) {
1141
1143
  return null;
1142
1144
  }
1143
1145
  }
@@ -1301,9 +1303,10 @@ function loadConfig(configPath) {
1301
1303
  }
1302
1304
  return loadConfigFromCwd();
1303
1305
  }
1306
+ var fsOverride = null;
1304
1307
  function loadConfigFromFile(configPath) {
1305
1308
  try {
1306
- const fs = require("fs");
1309
+ const fs = fsOverride || require("fs");
1307
1310
  if (!fs.existsSync(configPath)) {
1308
1311
  return defaultConfig;
1309
1312
  }
@@ -1315,7 +1318,7 @@ function loadConfigFromFile(configPath) {
1315
1318
  }
1316
1319
  function loadConfigFromCwd() {
1317
1320
  try {
1318
- const fs = require("fs");
1321
+ const fs = fsOverride || require("fs");
1319
1322
  const path = require("path");
1320
1323
  const candidates = [".flowlint.yml", ".flowlint.yaml", "flowlint.config.yml"];
1321
1324
  const cwd = process.cwd();