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,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileMatcher = void 0;
4
+ /**
5
+ * FileMatcher — matches changed files against decision patterns.
6
+ */
7
+ const minimatch_1 = require("minimatch");
8
+ const trie_1 = require("./trie");
9
+ const rule_evaluator_1 = require("./rule-evaluator");
10
+ const metrics_1 = require("./metrics");
11
+ class FileMatcher {
12
+ normalizedDecisions;
13
+ trie;
14
+ ruleEvaluator;
15
+ logger;
16
+ constructor(decisions, logger) {
17
+ this.logger = logger;
18
+ this.ruleEvaluator = new rule_evaluator_1.RuleEvaluator(logger);
19
+ this.normalizedDecisions = decisions.map((d) => ({
20
+ ...d,
21
+ files: d.files.map((f) => f.replace(/\\/g, '/').normalize('NFC')),
22
+ }));
23
+ const activeDecisions = this.normalizedDecisions.filter((d) => d.status === 'active');
24
+ this.trie = new trie_1.PatternTrie(activeDecisions);
25
+ }
26
+ /**
27
+ * Find matches using advanced rules
28
+ */
29
+ async findMatchesWithDiffs(fileDiffs) {
30
+ const activeDecisions = this.normalizedDecisions.filter((d) => d.status === 'active');
31
+ this.logger.debug(`FileMatcher: ${activeDecisions.length} active decisions loaded.`);
32
+ metrics_1.metrics.addDecisionsEvaluated(activeDecisions.length);
33
+ const matches = [];
34
+ const ruleDecisions = activeDecisions.filter((d) => d.rules);
35
+ const patternDecisions = activeDecisions.filter((d) => !d.rules);
36
+ if (patternDecisions.length > 0) {
37
+ const patternDecisionSet = new Set(patternDecisions);
38
+ const decisionMatches = new Map();
39
+ for (const fileDiff of fileDiffs) {
40
+ const normalizedFile = fileDiff.filename.replace(/\\/g, '/').normalize('NFC');
41
+ const candidates = this.trie.findCandidates(normalizedFile);
42
+ for (const decision of candidates) {
43
+ if (!patternDecisionSet.has(decision))
44
+ continue;
45
+ const matchedPattern = this.matchesDecision(normalizedFile, decision);
46
+ if (matchedPattern) {
47
+ if (!decisionMatches.has(decision)) {
48
+ decisionMatches.set(decision, { files: [], patterns: new Set() });
49
+ }
50
+ const matchData = decisionMatches.get(decision);
51
+ matchData.files.push(normalizedFile);
52
+ matchData.patterns.add(matchedPattern);
53
+ }
54
+ }
55
+ }
56
+ for (const [decision, data] of decisionMatches) {
57
+ for (const file of data.files) {
58
+ const matchedPattern = this.matchesDecision(file, decision);
59
+ if (matchedPattern) {
60
+ matches.push({
61
+ file,
62
+ decision,
63
+ matchedPattern,
64
+ matchDetails: {
65
+ matched: true,
66
+ matchedFiles: [file],
67
+ matchedPatterns: [matchedPattern],
68
+ ruleDepth: 0,
69
+ },
70
+ });
71
+ }
72
+ }
73
+ }
74
+ }
75
+ if (ruleDecisions.length > 0) {
76
+ const CONCURRENCY = 50;
77
+ const totalBatches = Math.ceil(ruleDecisions.length / CONCURRENCY);
78
+ for (let i = 0; i < ruleDecisions.length; i += CONCURRENCY) {
79
+ const batchNum = Math.floor(i / CONCURRENCY) + 1;
80
+ this.logger.debug(`Processing rule batch ${batchNum}/${totalBatches}...`);
81
+ const batch = ruleDecisions.slice(i, i + CONCURRENCY);
82
+ const batchResults = await Promise.allSettled(batch.map(async (decision) => {
83
+ const result = await this.ruleEvaluator.evaluate(decision.rules, fileDiffs);
84
+ if (result.matched) {
85
+ return {
86
+ file: result.matchedFiles.join(', '),
87
+ decision,
88
+ matchedPattern: result.matchedPatterns.slice(0, 3).join(', '),
89
+ matchDetails: result,
90
+ };
91
+ }
92
+ return null;
93
+ }));
94
+ const successfulResults = batchResults
95
+ .filter((r) => r.status === 'fulfilled' && r.value !== null)
96
+ .map((r) => r.value);
97
+ const failures = batchResults.filter((r) => r.status === 'rejected');
98
+ if (failures.length > 0) {
99
+ this.logger.warning(`${failures.length} decision evaluations failed in this batch. Check debug logs for details.`);
100
+ failures.forEach((f) => this.logger.debug(`Decision evaluation failed: ${f.reason}`));
101
+ }
102
+ matches.push(...successfulResults);
103
+ }
104
+ }
105
+ return matches.sort((a, b) => {
106
+ return activeDecisions.indexOf(a.decision) - activeDecisions.indexOf(b.decision);
107
+ });
108
+ }
109
+ /**
110
+ * Find all decisions that protect the given changed files
111
+ */
112
+ async findMatches(changedFiles) {
113
+ const activeDecisions = this.normalizedDecisions.filter((d) => d.status === 'active');
114
+ metrics_1.metrics.addDecisionsEvaluated(activeDecisions.length);
115
+ const CHUNK_SIZE = 500;
116
+ if (changedFiles.length > CHUNK_SIZE) {
117
+ const chunks = [];
118
+ for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
119
+ chunks.push(changedFiles.slice(i, i + CHUNK_SIZE));
120
+ }
121
+ const results = chunks.map((chunk) => this.processChunk(chunk));
122
+ return results.flat();
123
+ }
124
+ return this.processChunk(changedFiles);
125
+ }
126
+ processChunk(files) {
127
+ const matches = [];
128
+ for (const file of files) {
129
+ const normalizedFile = file.replace(/\\/g, '/');
130
+ const candidates = this.trie.findCandidates(normalizedFile);
131
+ for (const decision of candidates) {
132
+ const matchedPattern = this.matchesDecision(normalizedFile, decision);
133
+ if (matchedPattern) {
134
+ matches.push({ file: normalizedFile, decision, matchedPattern });
135
+ }
136
+ }
137
+ }
138
+ return matches;
139
+ }
140
+ /**
141
+ * Check if a file matches any pattern in a decision
142
+ */
143
+ matchesDecision(file, decision) {
144
+ let matchedPattern = null;
145
+ let isMatch = false;
146
+ for (const pattern of decision.files) {
147
+ if (pattern.startsWith('!')) {
148
+ if (this.matchesPattern(file, pattern.substring(1))) {
149
+ return null;
150
+ }
151
+ }
152
+ else {
153
+ if (this.matchesPattern(file, pattern)) {
154
+ isMatch = true;
155
+ matchedPattern = pattern;
156
+ }
157
+ }
158
+ }
159
+ return isMatch ? matchedPattern : null;
160
+ }
161
+ /**
162
+ * Check if a file matches a glob pattern
163
+ */
164
+ matchesPattern(file, pattern) {
165
+ const normalizedFile = file.normalize('NFC');
166
+ return (0, minimatch_1.minimatch)(normalizedFile, pattern, {
167
+ dot: true,
168
+ matchBase: false,
169
+ nocase: false,
170
+ nobrace: false,
171
+ });
172
+ }
173
+ /**
174
+ * Group matches by severity for prioritization
175
+ */
176
+ groupBySeverity(matches) {
177
+ return {
178
+ critical: matches.filter((m) => m.decision.severity === 'critical'),
179
+ warning: matches.filter((m) => m.decision.severity === 'warning'),
180
+ info: matches.filter((m) => m.decision.severity === 'info'),
181
+ };
182
+ }
183
+ }
184
+ exports.FileMatcher = FileMatcher;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * MetricsCollector — Platform-agnostic performance metrics.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.metrics = exports.MetricsCollector = void 0;
7
+ class MetricsCollector {
8
+ data = {
9
+ api_calls: 0,
10
+ api_errors: 0,
11
+ rate_limit_hits: 0,
12
+ files_processed: 0,
13
+ decisions_evaluated: 0,
14
+ matches_found: 0,
15
+ critical_matches: 0,
16
+ warning_matches: 0,
17
+ info_matches: 0,
18
+ duration_ms: 0,
19
+ parse_errors: 0,
20
+ parse_warnings: 0,
21
+ };
22
+ incrementApiCall() {
23
+ this.data.api_calls++;
24
+ }
25
+ incrementApiError() {
26
+ this.data.api_errors++;
27
+ }
28
+ incrementRateLimitHit() {
29
+ this.data.rate_limit_hits++;
30
+ }
31
+ addFilesProcessed(count) {
32
+ this.data.files_processed += count;
33
+ }
34
+ addDecisionsEvaluated(count) {
35
+ this.data.decisions_evaluated += count;
36
+ }
37
+ addMatchesFound(count) {
38
+ this.data.matches_found += count;
39
+ }
40
+ addCriticalMatches(count) {
41
+ this.data.critical_matches += count;
42
+ }
43
+ addWarningMatches(count) {
44
+ this.data.warning_matches += count;
45
+ }
46
+ addInfoMatches(count) {
47
+ this.data.info_matches += count;
48
+ }
49
+ setDuration(ms) {
50
+ this.data.duration_ms = ms;
51
+ }
52
+ addParseErrors(count) {
53
+ this.data.parse_errors += count;
54
+ }
55
+ addParseWarnings(count) {
56
+ this.data.parse_warnings += count;
57
+ }
58
+ /**
59
+ * Returns an immutable snapshot of collected metrics.
60
+ * Callers decide how to output: console, Actions output, telemetry, etc.
61
+ */
62
+ getSnapshot() {
63
+ return { ...this.data };
64
+ }
65
+ /**
66
+ * Reset all metrics to zero (useful for testing)
67
+ */
68
+ reset() {
69
+ this.data = {
70
+ api_calls: 0,
71
+ api_errors: 0,
72
+ rate_limit_hits: 0,
73
+ files_processed: 0,
74
+ decisions_evaluated: 0,
75
+ matches_found: 0,
76
+ critical_matches: 0,
77
+ warning_matches: 0,
78
+ info_matches: 0,
79
+ duration_ms: 0,
80
+ parse_errors: 0,
81
+ parse_warnings: 0,
82
+ };
83
+ }
84
+ }
85
+ exports.MetricsCollector = MetricsCollector;
86
+ /** Shared singleton instance */
87
+ exports.metrics = new MetricsCollector();
@@ -0,0 +1,338 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DecisionParser = void 0;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const rule_parser_1 = require("./rule-parser");
40
+ class DecisionParser {
41
+ ruleParser = new rule_parser_1.RuleParser();
42
+ STATUS_SYNONYMS = {
43
+ active: 'active',
44
+ enabled: 'active',
45
+ live: 'active',
46
+ deprecated: 'deprecated',
47
+ obsolete: 'deprecated',
48
+ superseded: 'superseded',
49
+ replaced: 'superseded',
50
+ archived: 'archived',
51
+ inactive: 'archived',
52
+ };
53
+ SEVERITY_SYNONYMS = {
54
+ info: 'info',
55
+ informational: 'info',
56
+ low: 'info',
57
+ warning: 'warning',
58
+ warn: 'warning',
59
+ medium: 'warning',
60
+ critical: 'critical',
61
+ error: 'critical',
62
+ high: 'critical',
63
+ blocker: 'critical',
64
+ };
65
+ /**
66
+ * Parse a decisions.md file
67
+ */
68
+ async parseFile(filePath) {
69
+ const workspaceRoot = process.env.GITHUB_WORKSPACE || process.cwd();
70
+ const resolvedPath = path.resolve(workspaceRoot, filePath);
71
+ const relativePath = path.relative(workspaceRoot, resolvedPath);
72
+ const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
73
+ if (!isSafe) {
74
+ return {
75
+ decisions: [],
76
+ errors: [
77
+ {
78
+ line: 0,
79
+ message: `Security: Path traversal detected - ${filePath}`,
80
+ },
81
+ ],
82
+ warnings: [],
83
+ };
84
+ }
85
+ try {
86
+ const stat = await fs.stat(resolvedPath);
87
+ if (stat.isDirectory()) {
88
+ return this.parseDirectory(resolvedPath);
89
+ }
90
+ const content = await fs.readFile(resolvedPath, 'utf-8');
91
+ return await this.parseContent(content, resolvedPath);
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ return {
96
+ decisions: [],
97
+ errors: [
98
+ {
99
+ line: 0,
100
+ message: `Failed to read file: ${message}`,
101
+ },
102
+ ],
103
+ warnings: [],
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * Recursively parse a directory for rule files
109
+ */
110
+ async parseDirectory(dirPath) {
111
+ const combinedResult = {
112
+ decisions: [],
113
+ errors: [],
114
+ warnings: [],
115
+ };
116
+ try {
117
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ const fullPath = path.join(dirPath, entry.name);
120
+ if (entry.isDirectory()) {
121
+ // Skip hidden directories like .git
122
+ if (entry.name.startsWith('.'))
123
+ continue;
124
+ const subResult = await this.parseDirectory(fullPath);
125
+ this.mergeResults(combinedResult, subResult);
126
+ }
127
+ else if (entry.isFile() &&
128
+ (entry.name.endsWith('.md') || entry.name.endsWith('.markdown'))) {
129
+ try {
130
+ const content = await fs.readFile(fullPath, 'utf-8');
131
+ const fileResult = await this.parseContent(content, fullPath);
132
+ this.mergeResults(combinedResult, fileResult);
133
+ }
134
+ catch (err) {
135
+ combinedResult.errors.push({
136
+ line: 0,
137
+ message: `Failed to parse ${entry.name}: ${err instanceof Error ? err.message : String(err)}`,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ catch (error) {
144
+ combinedResult.errors.push({
145
+ line: 0,
146
+ message: `Failed to list directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`,
147
+ });
148
+ }
149
+ return combinedResult;
150
+ }
151
+ mergeResults(target, source) {
152
+ target.decisions.push(...source.decisions);
153
+ target.errors.push(...source.errors);
154
+ target.warnings.push(...source.warnings);
155
+ }
156
+ /**
157
+ * Parse markdown content into decisions
158
+ */
159
+ async parseContent(content, sourceFile) {
160
+ const decisions = [];
161
+ const errors = [];
162
+ const warnings = [];
163
+ const blocks = this.splitIntoBlocks(content);
164
+ for (const block of blocks) {
165
+ try {
166
+ const decision = await this.parseBlock(block, sourceFile, warnings);
167
+ if (!decision.id || !decision.title) {
168
+ errors.push({
169
+ line: block.lineNumber,
170
+ message: `Decision missing required fields (id or title)`,
171
+ context: block.raw.substring(0, 100),
172
+ });
173
+ continue;
174
+ }
175
+ decisions.push(decision);
176
+ }
177
+ catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ errors.push({
180
+ line: block.lineNumber,
181
+ message,
182
+ context: block.raw.substring(0, 100),
183
+ });
184
+ }
185
+ }
186
+ return { decisions, errors, warnings };
187
+ }
188
+ /**
189
+ * Split content into decision blocks
190
+ */
191
+ splitIntoBlocks(content) {
192
+ if (!content.trim()) {
193
+ return [];
194
+ }
195
+ const blocks = [];
196
+ const markerPattern = /<!--\s*DECISION-(?:[A-Z0-9]+-)*[A-Z0-9]+\s*-->/gi;
197
+ let match;
198
+ const markers = [];
199
+ while ((match = markerPattern.exec(content)) !== null) {
200
+ markers.push(match.index);
201
+ }
202
+ // Split at markers
203
+ for (let i = 0; i < markers.length; i++) {
204
+ const start = markers[i];
205
+ const end = markers[i + 1] || content.length;
206
+ const blockContent = content.substring(start, end);
207
+ blocks.push({
208
+ raw: blockContent,
209
+ lineNumber: this.computeLineStart(content, start),
210
+ });
211
+ }
212
+ return blocks;
213
+ }
214
+ /**
215
+ * Compute the line number where a block starts
216
+ */
217
+ computeLineStart(fullContent, startIndex) {
218
+ const before = fullContent.substring(0, startIndex);
219
+ return before.split(/\r?\n/).length;
220
+ }
221
+ /**
222
+ * Parse a single decision block
223
+ */
224
+ async parseBlock(block, sourceFile, warnings) {
225
+ const content = block.raw;
226
+ const idMatch = content.match(/<!--\s*(DECISION-(?:[A-Z0-9]+-)*[A-Z0-9]+)\s*-->/i);
227
+ const id = idMatch ? idMatch[1].toUpperCase() : '';
228
+ const titleMatch = content.match(/^##\s*Decision:\s*(.+)$/im);
229
+ const title = titleMatch ? titleMatch[1].trim() : '';
230
+ const statusRaw = this.extractField(content, 'Status', 'active');
231
+ const date = this.extractField(content, 'Date', new Date().toISOString().split('T')[0]);
232
+ const severityRaw = this.extractField(content, 'Severity', 'info');
233
+ this.validateDate(date, id, warnings);
234
+ const files = this.extractFilesList(content);
235
+ const ruleResult = await this.ruleParser.extractRules(content, sourceFile);
236
+ if (ruleResult.error) {
237
+ warnings.push(`${id}: ${ruleResult.error}`);
238
+ }
239
+ const contextMatch = content.match(/###\s*Context\s*\n([\s\S]+?)(?=\n---+|\n<!--|$)/);
240
+ const context = contextMatch ? contextMatch[1].trim() : '';
241
+ return {
242
+ id,
243
+ title,
244
+ date,
245
+ status: this.normalizeStatus(statusRaw),
246
+ severity: this.normalizeSeverity(severityRaw),
247
+ schemaVersion: 1,
248
+ files,
249
+ rules: ruleResult.rules ?? undefined,
250
+ context,
251
+ sourceFile,
252
+ lineNumber: block.lineNumber,
253
+ };
254
+ }
255
+ /**
256
+ * Extract a metadata field like "**Status**: Active"
257
+ */
258
+ extractField(content, fieldName, defaultValue) {
259
+ const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
260
+ const regex = new RegExp(`^\\*\\*${escaped}\\*\\*:\\s*(.+)$`, 'im');
261
+ const match = content.match(regex);
262
+ return match ? match[1].trim() : defaultValue;
263
+ }
264
+ /**
265
+ * Extract list of file patterns
266
+ */
267
+ extractFilesList(content) {
268
+ const files = [];
269
+ const filesMatch = content.match(/\*\*Files\*\*:\s*\n/);
270
+ if (!filesMatch || filesMatch.index === undefined) {
271
+ return files;
272
+ }
273
+ const startPos = filesMatch.index + filesMatch[0].length;
274
+ const remainingContent = content.substring(startPos);
275
+ const lines = remainingContent.split('\n');
276
+ for (const line of lines) {
277
+ const withBackticks = line.match(/^\s*[-*]\s*`([^`]+)`\s*$/);
278
+ const withoutBackticks = line.match(/^\s*[-*]\s+([^\s`]+)\s*$/);
279
+ if (withBackticks) {
280
+ files.push(withBackticks[1].trim());
281
+ }
282
+ else if (withoutBackticks) {
283
+ files.push(withoutBackticks[1].trim());
284
+ }
285
+ else if (line.trim() !== '') {
286
+ break;
287
+ }
288
+ }
289
+ return files;
290
+ }
291
+ /**
292
+ * Validate date format and provide warnings
293
+ */
294
+ validateDate(dateString, decisionId, warnings) {
295
+ if (!dateString)
296
+ return;
297
+ const isoRegex = /^\d{4}-\d{2}-\d{2}$/;
298
+ if (!isoRegex.test(dateString)) {
299
+ warnings.push(`Decision ${decisionId}: Invalid date format '${dateString}' - use YYYY-MM-DD`);
300
+ return;
301
+ }
302
+ const parsed = new Date(dateString + 'T00:00:00Z');
303
+ if (isNaN(parsed.getTime())) {
304
+ warnings.push(`Decision ${decisionId}: Invalid date format '${dateString}' - use YYYY-MM-DD`);
305
+ return;
306
+ }
307
+ const [year, month, day] = dateString.split('-').map(Number);
308
+ if (parsed.getUTCFullYear() !== year ||
309
+ parsed.getUTCMonth() + 1 !== month ||
310
+ parsed.getUTCDate() !== day) {
311
+ warnings.push(`Decision ${decisionId}: Invalid date '${dateString}' (day doesn't exist)`);
312
+ return;
313
+ }
314
+ const now = new Date();
315
+ const tenYearsAgo = new Date(now.getFullYear() - 10, 0, 1);
316
+ if (parsed > now) {
317
+ warnings.push(`Decision ${decisionId}: Date is in the future - is this correct?`);
318
+ }
319
+ else if (parsed < tenYearsAgo) {
320
+ warnings.push(`Decision ${decisionId}: Date is >10 years old - consider archiving`);
321
+ }
322
+ }
323
+ /**
324
+ * Normalize status using synonyms
325
+ */
326
+ normalizeStatus(status) {
327
+ const normalized = status.toLowerCase().trim();
328
+ return this.STATUS_SYNONYMS[normalized] || 'active';
329
+ }
330
+ /**
331
+ * Normalize severity using synonyms
332
+ */
333
+ normalizeSeverity(severity) {
334
+ const normalized = severity.toLowerCase().trim();
335
+ return this.SEVERITY_SYNONYMS[normalized] || 'info';
336
+ }
337
+ }
338
+ exports.DecisionParser = DecisionParser;