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.
- package/LICENSE +21 -0
- package/README.md +792 -0
- package/dist/adapters/github/actions-logger.js +88 -0
- package/dist/adapters/github/comment.js +601 -0
- package/dist/adapters/github/github-provider.js +260 -0
- package/dist/adapters/github/health.js +56 -0
- package/dist/adapters/local/console-logger.js +46 -0
- package/dist/adapters/local/local-git-provider.js +247 -0
- package/dist/cli/commands/check.js +134 -0
- package/dist/cli/commands/init.js +58 -0
- package/dist/cli/commands/template.js +70 -0
- package/dist/cli/formatter.js +68 -0
- package/dist/cli/index.js +12458 -0
- package/dist/cli/licenses.txt +143 -0
- package/dist/cli/paths.js +40 -0
- package/dist/core/content-matchers.js +333 -0
- package/dist/core/health.js +52 -0
- package/dist/core/interfaces/index.js +2 -0
- package/dist/core/interfaces/logger.js +2 -0
- package/dist/core/interfaces/scm-provider.js +5 -0
- package/dist/core/logger.js +20 -0
- package/dist/core/matcher.js +184 -0
- package/dist/core/metrics.js +87 -0
- package/dist/core/parser.js +338 -0
- package/dist/core/rule-evaluator.js +186 -0
- package/dist/core/rule-parser.js +211 -0
- package/dist/core/rule-types.js +22 -0
- package/dist/core/trie.js +83 -0
- package/dist/core/types.js +2 -0
- package/dist/index.js +61142 -0
- package/dist/licenses.txt +758 -0
- package/dist/main.js +290 -0
- package/dist/telemetry/payload.js +25 -0
- package/dist/telemetry/privacy.js +37 -0
- package/dist/telemetry/sender.js +40 -0
- package/dist/version.js +7 -0
- package/package.json +60 -0
- package/templates/advanced-rules.md +94 -0
- package/templates/api.md +70 -0
- package/templates/basic.md +38 -0
- package/templates/database.md +81 -0
- 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;
|