decision-guardian 1.1.0

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +792 -0
  3. package/dist/adapters/github/actions-logger.js +88 -0
  4. package/dist/adapters/github/comment.js +601 -0
  5. package/dist/adapters/github/github-provider.js +260 -0
  6. package/dist/adapters/github/health.js +56 -0
  7. package/dist/adapters/local/console-logger.js +46 -0
  8. package/dist/adapters/local/local-git-provider.js +247 -0
  9. package/dist/cli/commands/check.js +134 -0
  10. package/dist/cli/commands/init.js +58 -0
  11. package/dist/cli/commands/template.js +70 -0
  12. package/dist/cli/formatter.js +68 -0
  13. package/dist/cli/index.js +12458 -0
  14. package/dist/cli/licenses.txt +143 -0
  15. package/dist/cli/paths.js +40 -0
  16. package/dist/core/content-matchers.js +333 -0
  17. package/dist/core/health.js +52 -0
  18. package/dist/core/interfaces/index.js +2 -0
  19. package/dist/core/interfaces/logger.js +2 -0
  20. package/dist/core/interfaces/scm-provider.js +5 -0
  21. package/dist/core/logger.js +20 -0
  22. package/dist/core/matcher.js +184 -0
  23. package/dist/core/metrics.js +87 -0
  24. package/dist/core/parser.js +338 -0
  25. package/dist/core/rule-evaluator.js +186 -0
  26. package/dist/core/rule-parser.js +211 -0
  27. package/dist/core/rule-types.js +22 -0
  28. package/dist/core/trie.js +83 -0
  29. package/dist/core/types.js +2 -0
  30. package/dist/index.js +61142 -0
  31. package/dist/licenses.txt +758 -0
  32. package/dist/main.js +290 -0
  33. package/dist/telemetry/payload.js +25 -0
  34. package/dist/telemetry/privacy.js +37 -0
  35. package/dist/telemetry/sender.js +40 -0
  36. package/dist/version.js +7 -0
  37. package/package.json +60 -0
  38. package/templates/advanced-rules.md +94 -0
  39. package/templates/api.md +70 -0
  40. package/templates/basic.md +38 -0
  41. package/templates/database.md +81 -0
  42. package/templates/security.md +89 -0
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuleEvaluator = void 0;
4
+ /**
5
+ * Rule Evaluator - Evaluates decision rules against file diffs
6
+ */
7
+ const minimatch_1 = require("minimatch");
8
+ const rule_types_1 = require("./rule-types");
9
+ const content_matchers_1 = require("./content-matchers");
10
+ class RuleEvaluator {
11
+ contentMatchers;
12
+ logger;
13
+ constructor(logger) {
14
+ this.logger = logger;
15
+ this.contentMatchers = new content_matchers_1.ContentMatchers(logger);
16
+ }
17
+ /**
18
+ * Evaluate if a changeset matches the decision rules
19
+ */
20
+ async evaluate(rules, fileDiffs, depth = 0) {
21
+ // Depth safety check
22
+ if (depth > rule_types_1.MAX_RULE_DEPTH) {
23
+ return {
24
+ matched: false,
25
+ matchedPatterns: [],
26
+ matchedFiles: [],
27
+ ruleDepth: depth,
28
+ error: `Rule nesting exceeds max depth of ${rule_types_1.MAX_RULE_DEPTH}`,
29
+ };
30
+ }
31
+ const matchMode = rules.match_mode || 'any';
32
+ // Check if this is a single-rule case
33
+ if (rules.pattern && !rules.conditions) {
34
+ return this.evaluateSingleRule(rules, fileDiffs, depth);
35
+ }
36
+ if (!rules.conditions || rules.conditions.length === 0) {
37
+ return {
38
+ matched: false,
39
+ matchedPatterns: [],
40
+ matchedFiles: [],
41
+ ruleDepth: depth,
42
+ };
43
+ }
44
+ const settlement = await Promise.allSettled(rules.conditions.map((condition) => {
45
+ if ((0, rule_types_1.isFileRule)(condition)) {
46
+ return this.evaluateSingleRule(condition, fileDiffs, depth + 1);
47
+ }
48
+ else {
49
+ return this.evaluate(condition, fileDiffs, depth + 1);
50
+ }
51
+ }));
52
+ const results = settlement.map((r) => r.status === 'fulfilled'
53
+ ? r.value
54
+ : {
55
+ matched: false,
56
+ matchedPatterns: [],
57
+ matchedFiles: [],
58
+ ruleDepth: depth + 1,
59
+ error: `Condition evaluation failed: ${r.reason}`,
60
+ });
61
+ const matched = matchMode === 'all'
62
+ ? results.every((r) => r.matched) // AND
63
+ : results.some((r) => r.matched); // OR
64
+ const matchedPatterns = results.flatMap((r) => r.matchedPatterns).sort();
65
+ const matchedFiles = [...new Set(results.flatMap((r) => r.matchedFiles))].sort();
66
+ const errors = results
67
+ .map((r) => r.error)
68
+ .filter(Boolean)
69
+ .join('; ');
70
+ return {
71
+ matched,
72
+ matchedPatterns,
73
+ matchedFiles,
74
+ ruleDepth: depth,
75
+ error: errors || undefined,
76
+ };
77
+ }
78
+ /**
79
+ * Evaluate a single file rule with error boundary
80
+ */
81
+ async evaluateSingleRule(rule, fileDiffs, depth) {
82
+ try {
83
+ const matchingFiles = fileDiffs.filter((file) => {
84
+ const matches = (0, minimatch_1.minimatch)(file.filename, rule.pattern, {
85
+ dot: true,
86
+ matchBase: false,
87
+ nocase: false,
88
+ });
89
+ if (matches && rule.exclude) {
90
+ // Handle both string and string[] exclude patterns
91
+ const excludePatterns = Array.isArray(rule.exclude) ? rule.exclude : [rule.exclude];
92
+ const isExcluded = excludePatterns.some((pattern) => (0, minimatch_1.minimatch)(file.filename, pattern, {
93
+ dot: true,
94
+ matchBase: false,
95
+ nocase: false,
96
+ }));
97
+ return !isExcluded;
98
+ }
99
+ return matches;
100
+ });
101
+ if (matchingFiles.length === 0) {
102
+ this.logger.debug(`RuleEvaluator: No files matched pattern '${rule.pattern}'`);
103
+ return {
104
+ matched: false,
105
+ matchedPatterns: [],
106
+ matchedFiles: [],
107
+ ruleDepth: depth,
108
+ };
109
+ }
110
+ if (!rule.content_rules || rule.content_rules.length === 0) {
111
+ this.logger.debug(`RuleEvaluator: File '${rule.pattern}' matched ${matchingFiles.length} files: ${matchingFiles.map((f) => f.filename).join(', ')}`);
112
+ return {
113
+ matched: true,
114
+ matchedPatterns: [rule.pattern],
115
+ matchedFiles: matchingFiles.map((f) => f.filename),
116
+ ruleDepth: depth,
117
+ };
118
+ }
119
+ this.logger.debug(`RuleEvaluator: File '${rule.pattern}' matched ${matchingFiles.length} files, checking content rules...`);
120
+ const allMatchedPatterns = [];
121
+ const allMatchedFiles = [];
122
+ for (const file of matchingFiles) {
123
+ const contentResult = await this.evaluateContentRules(rule.content_rules, file);
124
+ if (contentResult.matched) {
125
+ allMatchedPatterns.push(...contentResult.matchedPatterns);
126
+ allMatchedFiles.push(file.filename);
127
+ }
128
+ }
129
+ return {
130
+ matched: allMatchedFiles.length > 0,
131
+ matchedPatterns: [...new Set(allMatchedPatterns)].sort(),
132
+ matchedFiles: allMatchedFiles.sort(),
133
+ ruleDepth: depth,
134
+ };
135
+ }
136
+ catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ this.logger.warning(`Rule evaluation failed for pattern "${rule.pattern}": ${message}`);
139
+ return {
140
+ matched: false,
141
+ matchedPatterns: [],
142
+ matchedFiles: [],
143
+ ruleDepth: depth,
144
+ error: message,
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Evaluate content rules against a file diff
150
+ */
151
+ async evaluateContentRules(rules, file) {
152
+ const allMatchedPatterns = [];
153
+ for (const rule of rules) {
154
+ let result;
155
+ switch (rule.mode) {
156
+ case 'string':
157
+ result = this.contentMatchers.matchString(rule, file);
158
+ break;
159
+ case 'regex':
160
+ result = await this.contentMatchers.matchRegex(rule, file);
161
+ break;
162
+ case 'line_range':
163
+ result = this.contentMatchers.matchLineRange(rule, file);
164
+ break;
165
+ case 'full_file':
166
+ result = this.contentMatchers.matchFullFile(file);
167
+ break;
168
+ case 'json_path':
169
+ result = this.contentMatchers.matchJsonPath(rule, file);
170
+ break;
171
+ default: {
172
+ const _exhaustiveCheck = rule.mode;
173
+ throw new Error(`Unhandled content match mode: ${_exhaustiveCheck}`);
174
+ }
175
+ }
176
+ if (result.matched) {
177
+ allMatchedPatterns.push(...result.matchedPatterns);
178
+ }
179
+ }
180
+ return {
181
+ matched: allMatchedPatterns.length > 0,
182
+ matchedPatterns: allMatchedPatterns.sort(),
183
+ };
184
+ }
185
+ }
186
+ exports.RuleEvaluator = RuleEvaluator;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.RuleParser = void 0;
40
+ /**
41
+ * Rule Parser - Extracts JSON rules from markdown decision blocks
42
+ */
43
+ const fs = __importStar(require("fs/promises"));
44
+ const safe_regex_1 = __importDefault(require("safe-regex"));
45
+ const path = __importStar(require("path"));
46
+ const rule_types_1 = require("./rule-types");
47
+ class RuleParser {
48
+ /**
49
+ * Extract JSON rules from markdown content
50
+ * Supports:
51
+ * 1. Inline JSON: **Rules**: followed by ```json ... ```
52
+ * 2. External File: **Rules**: [Link](./path) or just path
53
+ */
54
+ async extractRules(content, sourceFilePath) {
55
+ // 1. Try inline JSON first
56
+ const rulesMatch = content.match(/\*\*Rules\*\*:\s*```json\s+([\s\S]+?)\s+```/i);
57
+ if (rulesMatch) {
58
+ try {
59
+ const parsed = JSON.parse(rulesMatch[1]);
60
+ const validated = this.validate(parsed, 0);
61
+ return { rules: validated };
62
+ }
63
+ catch (error) {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ return {
66
+ rules: null,
67
+ error: `Failed to parse inline JSON rules: ${message}`,
68
+ };
69
+ }
70
+ }
71
+ // 2. Try external file reference
72
+ // Matches: **Rules**: [Label](path) or **Rules**: path/to/file.json
73
+ const linkMatch = content.match(/\*\*Rules\*\*:\s*(?:\[.*?\]\((.*?)\)|(\S+\.json))/i);
74
+ if (linkMatch) {
75
+ const relPath = linkMatch[1] || linkMatch[2];
76
+ try {
77
+ const workspaceRoot = process.env.GITHUB_WORKSPACE || process.cwd();
78
+ const sourceDir = path.dirname(sourceFilePath);
79
+ // Resolve path relative to the decision file
80
+ const resolvedPath = path.resolve(sourceDir, relPath);
81
+ const normalizedWorkspace = path.normalize(workspaceRoot);
82
+ const normalizedPath = path.normalize(resolvedPath);
83
+ // Security check: Reject paths outside workspace (Path Traversal protection)
84
+ // We also strictly reject Windows-specific absolute paths (like C:\...) on non-Windows platforms
85
+ // to prevent them from being interpreted as relative filenames
86
+ const isWindowsSpecificAbsolute = path.win32.isAbsolute(relPath) && !path.posix.isAbsolute(relPath);
87
+ const isCrossPlatformAbsolute = process.platform !== 'win32' && isWindowsSpecificAbsolute;
88
+ if ((!resolvedPath.startsWith(normalizedWorkspace + path.sep) &&
89
+ resolvedPath !== normalizedWorkspace) ||
90
+ isCrossPlatformAbsolute) {
91
+ return {
92
+ rules: null,
93
+ error: `Security Error: External rule file '${relPath}' resolves to a path outside the workspace. ` +
94
+ `Only files within the workspace are allowed. ` +
95
+ `Resolved: ${normalizedPath}, Workspace: ${normalizedWorkspace}`,
96
+ };
97
+ }
98
+ const fileContent = await fs.readFile(resolvedPath, 'utf-8');
99
+ const parsed = JSON.parse(fileContent);
100
+ const validated = this.validate(parsed, 0);
101
+ return { rules: validated };
102
+ }
103
+ catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ return {
106
+ rules: null,
107
+ error: `Failed to load external rules from ${relPath}: ${message}`,
108
+ };
109
+ }
110
+ }
111
+ return { rules: null };
112
+ }
113
+ /**
114
+ * Validate rule structure with depth tracking
115
+ */
116
+ validate(rules, depth) {
117
+ if (depth > rule_types_1.MAX_RULE_DEPTH) {
118
+ throw new Error(`Rule nesting exceeds max depth of ${rule_types_1.MAX_RULE_DEPTH}`);
119
+ }
120
+ if (!rules.match_mode) {
121
+ rules.match_mode = 'any';
122
+ }
123
+ if (rules.pattern && !rules.conditions) {
124
+ this.validateFileRule(rules);
125
+ return rules;
126
+ }
127
+ if (rules.conditions && Array.isArray(rules.conditions)) {
128
+ for (const condition of rules.conditions) {
129
+ if ((0, rule_types_1.isFileRule)(condition)) {
130
+ this.validateFileRule(condition);
131
+ }
132
+ else {
133
+ this.validate(condition, depth + 1);
134
+ }
135
+ }
136
+ }
137
+ return rules;
138
+ }
139
+ /**
140
+ * Validate a file rule
141
+ */
142
+ validateFileRule(rule) {
143
+ if (!rule.pattern) {
144
+ throw new Error('FileRule must have a pattern');
145
+ }
146
+ if (rule.content_rules && Array.isArray(rule.content_rules)) {
147
+ for (const contentRule of rule.content_rules) {
148
+ this.validateContentRule(contentRule);
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Validate a content rule
154
+ */
155
+ validateContentRule(rule) {
156
+ const validModes = ['string', 'regex', 'line_range', 'full_file', 'json_path'];
157
+ if (!validModes.includes(rule.mode)) {
158
+ throw new Error(`Invalid content rule mode: ${rule.mode}`);
159
+ }
160
+ switch (rule.mode) {
161
+ case 'string': {
162
+ if (!rule.patterns || !Array.isArray(rule.patterns)) {
163
+ throw new Error('String mode requires patterns array');
164
+ }
165
+ break;
166
+ }
167
+ case 'regex': {
168
+ if (!rule.pattern) {
169
+ throw new Error('Regex mode requires pattern');
170
+ }
171
+ let isSafe;
172
+ try {
173
+ isSafe = (0, safe_regex_1.default)(rule.pattern);
174
+ }
175
+ catch (e) {
176
+ throw new Error(`Invalid regex pattern (safe-check failed): ${rule.pattern}`);
177
+ }
178
+ if (!isSafe) {
179
+ throw new Error(`Unsafe regex pattern: ${rule.pattern}`);
180
+ }
181
+ const ALLOWED_FLAGS = /^[gimsuy]*$/;
182
+ if (rule.flags && !ALLOWED_FLAGS.test(rule.flags)) {
183
+ throw new Error(`Invalid regex flags: ${rule.flags}`);
184
+ }
185
+ try {
186
+ new RegExp(rule.pattern, rule.flags || '');
187
+ }
188
+ catch (e) {
189
+ throw new Error(`Invalid regex pattern syntax: ${rule.pattern}`);
190
+ }
191
+ break;
192
+ }
193
+ case 'line_range':
194
+ if (typeof rule.start !== 'number' || typeof rule.end !== 'number') {
195
+ throw new Error('Line range mode requires start and end numbers');
196
+ }
197
+ if (rule.start > rule.end) {
198
+ throw new Error('Line range start must be <= end');
199
+ }
200
+ break;
201
+ case 'json_path':
202
+ if (!rule.paths || !Array.isArray(rule.paths)) {
203
+ throw new Error('JSON path mode requires paths array');
204
+ }
205
+ break;
206
+ case 'full_file':
207
+ break;
208
+ }
209
+ }
210
+ }
211
+ exports.RuleParser = RuleParser;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Rule Types for Advanced Decision Rules System
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_RULE_DEPTH = void 0;
7
+ exports.isFileRule = isFileRule;
8
+ exports.isRuleCondition = isRuleCondition;
9
+ /** Safety limit for nested rules to prevent stack overflow */
10
+ exports.MAX_RULE_DEPTH = 10;
11
+ /**
12
+ * Type guard to check if a condition is a FileRule
13
+ */
14
+ function isFileRule(condition) {
15
+ return (condition.type === 'file' && typeof condition.pattern === 'string');
16
+ }
17
+ /**
18
+ * Type guard to check if a condition is a nested RuleCondition
19
+ */
20
+ function isRuleCondition(condition) {
21
+ return Array.isArray(condition.conditions);
22
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PatternTrie = void 0;
4
+ class PatternTrie {
5
+ root;
6
+ constructor(decisions) {
7
+ this.root = this.createNode();
8
+ for (const decision of decisions) {
9
+ for (const pattern of decision.files) {
10
+ if (!pattern.startsWith('!')) {
11
+ this.insert(pattern, decision);
12
+ }
13
+ }
14
+ }
15
+ }
16
+ createNode() {
17
+ return {
18
+ children: new Map(),
19
+ decisions: [],
20
+ wildcardDecisions: [],
21
+ };
22
+ }
23
+ insert(pattern, decision) {
24
+ const parts = pattern.split('/');
25
+ this.insertRecursive(this.root, parts, decision);
26
+ }
27
+ insertRecursive(node, parts, decision) {
28
+ if (parts.length === 0) {
29
+ node.decisions.push(decision);
30
+ return;
31
+ }
32
+ const part = parts[0];
33
+ const remaining = parts.slice(1);
34
+ if (part === '**') {
35
+ node.wildcardDecisions.push(decision);
36
+ if (remaining.length > 0) {
37
+ this.insertRecursive(node, remaining, decision);
38
+ }
39
+ return;
40
+ }
41
+ if (part.includes('*') ||
42
+ part.includes('?') ||
43
+ part.includes('{') ||
44
+ part.includes('}') ||
45
+ part.includes('[') ||
46
+ part.includes(']')) {
47
+ node.wildcardDecisions.push(decision);
48
+ return;
49
+ }
50
+ let child = node.children.get(part);
51
+ if (!child) {
52
+ child = this.createNode();
53
+ node.children.set(part, child);
54
+ }
55
+ this.insertRecursive(child, remaining, decision);
56
+ }
57
+ /**
58
+ * Returns a set of unique decisions that *might* match the given file path.
59
+ */
60
+ findCandidates(file) {
61
+ const parts = file.split('/');
62
+ const candidates = new Set();
63
+ this.collectCandidates(this.root, parts, candidates);
64
+ return candidates;
65
+ }
66
+ collectCandidates(node, parts, candidates) {
67
+ for (const decision of node.wildcardDecisions) {
68
+ candidates.add(decision);
69
+ }
70
+ if (parts.length === 0) {
71
+ for (const decision of node.decisions) {
72
+ candidates.add(decision);
73
+ }
74
+ return;
75
+ }
76
+ const part = parts[0];
77
+ const child = node.children.get(part);
78
+ if (child) {
79
+ this.collectCandidates(child, parts.slice(1), candidates);
80
+ }
81
+ }
82
+ }
83
+ exports.PatternTrie = PatternTrie;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });