driftdetect-core 0.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 (221) hide show
  1. package/dist/analyzers/ast-analyzer.d.ts +251 -0
  2. package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
  3. package/dist/analyzers/ast-analyzer.js +548 -0
  4. package/dist/analyzers/ast-analyzer.js.map +1 -0
  5. package/dist/analyzers/flow-analyzer.d.ts +241 -0
  6. package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
  7. package/dist/analyzers/flow-analyzer.js +1219 -0
  8. package/dist/analyzers/flow-analyzer.js.map +1 -0
  9. package/dist/analyzers/index.d.ts +18 -0
  10. package/dist/analyzers/index.d.ts.map +1 -0
  11. package/dist/analyzers/index.js +19 -0
  12. package/dist/analyzers/index.js.map +1 -0
  13. package/dist/analyzers/semantic-analyzer.d.ts +252 -0
  14. package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
  15. package/dist/analyzers/semantic-analyzer.js +1182 -0
  16. package/dist/analyzers/semantic-analyzer.js.map +1 -0
  17. package/dist/analyzers/type-analyzer.d.ts +289 -0
  18. package/dist/analyzers/type-analyzer.d.ts.map +1 -0
  19. package/dist/analyzers/type-analyzer.js +1269 -0
  20. package/dist/analyzers/type-analyzer.js.map +1 -0
  21. package/dist/analyzers/types.d.ts +537 -0
  22. package/dist/analyzers/types.d.ts.map +1 -0
  23. package/dist/analyzers/types.js +11 -0
  24. package/dist/analyzers/types.js.map +1 -0
  25. package/dist/config/config-loader.d.ts +166 -0
  26. package/dist/config/config-loader.d.ts.map +1 -0
  27. package/dist/config/config-loader.js +429 -0
  28. package/dist/config/config-loader.js.map +1 -0
  29. package/dist/config/config-validator.d.ts +204 -0
  30. package/dist/config/config-validator.d.ts.map +1 -0
  31. package/dist/config/config-validator.js +632 -0
  32. package/dist/config/config-validator.js.map +1 -0
  33. package/dist/config/defaults.d.ts +8 -0
  34. package/dist/config/defaults.d.ts.map +1 -0
  35. package/dist/config/defaults.js +26 -0
  36. package/dist/config/defaults.js.map +1 -0
  37. package/dist/config/index.d.ts +10 -0
  38. package/dist/config/index.d.ts.map +1 -0
  39. package/dist/config/index.js +10 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/config/types.d.ts +47 -0
  42. package/dist/config/types.d.ts.map +1 -0
  43. package/dist/config/types.js +7 -0
  44. package/dist/config/types.js.map +1 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/manifest/exporter.d.ts +21 -0
  50. package/dist/manifest/exporter.d.ts.map +1 -0
  51. package/dist/manifest/exporter.js +339 -0
  52. package/dist/manifest/exporter.js.map +1 -0
  53. package/dist/manifest/index.d.ts +14 -0
  54. package/dist/manifest/index.d.ts.map +1 -0
  55. package/dist/manifest/index.js +15 -0
  56. package/dist/manifest/index.js.map +1 -0
  57. package/dist/manifest/manifest-store.d.ts +111 -0
  58. package/dist/manifest/manifest-store.d.ts.map +1 -0
  59. package/dist/manifest/manifest-store.js +418 -0
  60. package/dist/manifest/manifest-store.js.map +1 -0
  61. package/dist/manifest/types.d.ts +238 -0
  62. package/dist/manifest/types.d.ts.map +1 -0
  63. package/dist/manifest/types.js +11 -0
  64. package/dist/manifest/types.js.map +1 -0
  65. package/dist/matcher/confidence-scorer.d.ts +188 -0
  66. package/dist/matcher/confidence-scorer.d.ts.map +1 -0
  67. package/dist/matcher/confidence-scorer.js +302 -0
  68. package/dist/matcher/confidence-scorer.js.map +1 -0
  69. package/dist/matcher/index.d.ts +24 -0
  70. package/dist/matcher/index.d.ts.map +1 -0
  71. package/dist/matcher/index.js +26 -0
  72. package/dist/matcher/index.js.map +1 -0
  73. package/dist/matcher/outlier-detector.d.ts +252 -0
  74. package/dist/matcher/outlier-detector.d.ts.map +1 -0
  75. package/dist/matcher/outlier-detector.js +544 -0
  76. package/dist/matcher/outlier-detector.js.map +1 -0
  77. package/dist/matcher/pattern-matcher.d.ts +169 -0
  78. package/dist/matcher/pattern-matcher.d.ts.map +1 -0
  79. package/dist/matcher/pattern-matcher.js +692 -0
  80. package/dist/matcher/pattern-matcher.js.map +1 -0
  81. package/dist/matcher/types.d.ts +476 -0
  82. package/dist/matcher/types.d.ts.map +1 -0
  83. package/dist/matcher/types.js +36 -0
  84. package/dist/matcher/types.js.map +1 -0
  85. package/dist/parsers/base-parser.d.ts +282 -0
  86. package/dist/parsers/base-parser.d.ts.map +1 -0
  87. package/dist/parsers/base-parser.js +421 -0
  88. package/dist/parsers/base-parser.js.map +1 -0
  89. package/dist/parsers/css-parser.d.ts +225 -0
  90. package/dist/parsers/css-parser.d.ts.map +1 -0
  91. package/dist/parsers/css-parser.js +477 -0
  92. package/dist/parsers/css-parser.js.map +1 -0
  93. package/dist/parsers/index.d.ts +15 -0
  94. package/dist/parsers/index.d.ts.map +1 -0
  95. package/dist/parsers/index.js +15 -0
  96. package/dist/parsers/index.js.map +1 -0
  97. package/dist/parsers/json-parser.d.ts +219 -0
  98. package/dist/parsers/json-parser.d.ts.map +1 -0
  99. package/dist/parsers/json-parser.js +602 -0
  100. package/dist/parsers/json-parser.js.map +1 -0
  101. package/dist/parsers/markdown-parser.d.ts +276 -0
  102. package/dist/parsers/markdown-parser.d.ts.map +1 -0
  103. package/dist/parsers/markdown-parser.js +731 -0
  104. package/dist/parsers/markdown-parser.js.map +1 -0
  105. package/dist/parsers/parser-manager.d.ts +294 -0
  106. package/dist/parsers/parser-manager.d.ts.map +1 -0
  107. package/dist/parsers/parser-manager.js +738 -0
  108. package/dist/parsers/parser-manager.js.map +1 -0
  109. package/dist/parsers/python-parser.d.ts +204 -0
  110. package/dist/parsers/python-parser.d.ts.map +1 -0
  111. package/dist/parsers/python-parser.js +517 -0
  112. package/dist/parsers/python-parser.js.map +1 -0
  113. package/dist/parsers/types.d.ts +43 -0
  114. package/dist/parsers/types.d.ts.map +1 -0
  115. package/dist/parsers/types.js +7 -0
  116. package/dist/parsers/types.js.map +1 -0
  117. package/dist/parsers/typescript-parser.d.ts +264 -0
  118. package/dist/parsers/typescript-parser.d.ts.map +1 -0
  119. package/dist/parsers/typescript-parser.js +658 -0
  120. package/dist/parsers/typescript-parser.js.map +1 -0
  121. package/dist/rules/evaluator.d.ts +305 -0
  122. package/dist/rules/evaluator.d.ts.map +1 -0
  123. package/dist/rules/evaluator.js +579 -0
  124. package/dist/rules/evaluator.js.map +1 -0
  125. package/dist/rules/index.d.ts +13 -0
  126. package/dist/rules/index.d.ts.map +1 -0
  127. package/dist/rules/index.js +13 -0
  128. package/dist/rules/index.js.map +1 -0
  129. package/dist/rules/quick-fix-generator.d.ts +334 -0
  130. package/dist/rules/quick-fix-generator.d.ts.map +1 -0
  131. package/dist/rules/quick-fix-generator.js +1075 -0
  132. package/dist/rules/quick-fix-generator.js.map +1 -0
  133. package/dist/rules/rule-engine.d.ts +241 -0
  134. package/dist/rules/rule-engine.d.ts.map +1 -0
  135. package/dist/rules/rule-engine.js +585 -0
  136. package/dist/rules/rule-engine.js.map +1 -0
  137. package/dist/rules/severity-manager.d.ts +394 -0
  138. package/dist/rules/severity-manager.d.ts.map +1 -0
  139. package/dist/rules/severity-manager.js +619 -0
  140. package/dist/rules/severity-manager.js.map +1 -0
  141. package/dist/rules/types.d.ts +370 -0
  142. package/dist/rules/types.d.ts.map +1 -0
  143. package/dist/rules/types.js +133 -0
  144. package/dist/rules/types.js.map +1 -0
  145. package/dist/rules/variant-manager.d.ts +388 -0
  146. package/dist/rules/variant-manager.d.ts.map +1 -0
  147. package/dist/rules/variant-manager.js +777 -0
  148. package/dist/rules/variant-manager.js.map +1 -0
  149. package/dist/scanner/change-detector.d.ts +164 -0
  150. package/dist/scanner/change-detector.d.ts.map +1 -0
  151. package/dist/scanner/change-detector.js +263 -0
  152. package/dist/scanner/change-detector.js.map +1 -0
  153. package/dist/scanner/dependency-graph.d.ts +270 -0
  154. package/dist/scanner/dependency-graph.d.ts.map +1 -0
  155. package/dist/scanner/dependency-graph.js +436 -0
  156. package/dist/scanner/dependency-graph.js.map +1 -0
  157. package/dist/scanner/file-walker.d.ts +127 -0
  158. package/dist/scanner/file-walker.d.ts.map +1 -0
  159. package/dist/scanner/file-walker.js +526 -0
  160. package/dist/scanner/file-walker.js.map +1 -0
  161. package/dist/scanner/index.d.ts +12 -0
  162. package/dist/scanner/index.d.ts.map +1 -0
  163. package/dist/scanner/index.js +12 -0
  164. package/dist/scanner/index.js.map +1 -0
  165. package/dist/scanner/types.d.ts +218 -0
  166. package/dist/scanner/types.d.ts.map +1 -0
  167. package/dist/scanner/types.js +10 -0
  168. package/dist/scanner/types.js.map +1 -0
  169. package/dist/scanner/worker-pool.d.ts +317 -0
  170. package/dist/scanner/worker-pool.d.ts.map +1 -0
  171. package/dist/scanner/worker-pool.js +571 -0
  172. package/dist/scanner/worker-pool.js.map +1 -0
  173. package/dist/store/cache-manager.d.ts +179 -0
  174. package/dist/store/cache-manager.d.ts.map +1 -0
  175. package/dist/store/cache-manager.js +391 -0
  176. package/dist/store/cache-manager.js.map +1 -0
  177. package/dist/store/history-store.d.ts +314 -0
  178. package/dist/store/history-store.d.ts.map +1 -0
  179. package/dist/store/history-store.js +707 -0
  180. package/dist/store/history-store.js.map +1 -0
  181. package/dist/store/index.d.ts +20 -0
  182. package/dist/store/index.d.ts.map +1 -0
  183. package/dist/store/index.js +26 -0
  184. package/dist/store/index.js.map +1 -0
  185. package/dist/store/lock-file-manager.d.ts +202 -0
  186. package/dist/store/lock-file-manager.d.ts.map +1 -0
  187. package/dist/store/lock-file-manager.js +475 -0
  188. package/dist/store/lock-file-manager.js.map +1 -0
  189. package/dist/store/pattern-store.d.ts +289 -0
  190. package/dist/store/pattern-store.d.ts.map +1 -0
  191. package/dist/store/pattern-store.js +936 -0
  192. package/dist/store/pattern-store.js.map +1 -0
  193. package/dist/store/schema-validator.d.ts +159 -0
  194. package/dist/store/schema-validator.d.ts.map +1 -0
  195. package/dist/store/schema-validator.js +1096 -0
  196. package/dist/store/schema-validator.js.map +1 -0
  197. package/dist/store/types.d.ts +585 -0
  198. package/dist/store/types.d.ts.map +1 -0
  199. package/dist/store/types.js +82 -0
  200. package/dist/store/types.js.map +1 -0
  201. package/dist/types/analysis.d.ts +19 -0
  202. package/dist/types/analysis.d.ts.map +1 -0
  203. package/dist/types/analysis.js +5 -0
  204. package/dist/types/analysis.js.map +1 -0
  205. package/dist/types/common.d.ts +7 -0
  206. package/dist/types/common.d.ts.map +1 -0
  207. package/dist/types/common.js +5 -0
  208. package/dist/types/common.js.map +1 -0
  209. package/dist/types/index.d.ts +12 -0
  210. package/dist/types/index.d.ts.map +1 -0
  211. package/dist/types/index.js +10 -0
  212. package/dist/types/index.js.map +1 -0
  213. package/dist/types/patterns.d.ts +40 -0
  214. package/dist/types/patterns.d.ts.map +1 -0
  215. package/dist/types/patterns.js +7 -0
  216. package/dist/types/patterns.js.map +1 -0
  217. package/dist/types/violations.d.ts +7 -0
  218. package/dist/types/violations.d.ts.map +1 -0
  219. package/dist/types/violations.js +7 -0
  220. package/dist/types/violations.js.map +1 -0
  221. package/package.json +46 -0
@@ -0,0 +1,1096 @@
1
+ /**
2
+ * Schema Validator - JSON schema validation for patterns and config
3
+ *
4
+ * Provides comprehensive JSON schema validation for all store data structures
5
+ * including patterns, pattern files, history files, lock files, and variants.
6
+ *
7
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
8
+ */
9
+ // ============================================================================
10
+ // Schema Version Constants
11
+ // ============================================================================
12
+ /**
13
+ * Current schema versions for validation
14
+ */
15
+ export const SCHEMA_VERSIONS = {
16
+ pattern: '1.0.0',
17
+ patternFile: '1.0.0',
18
+ historyFile: '1.0.0',
19
+ lockFile: '1.0.0',
20
+ variantsFile: '1.0.0',
21
+ config: '1.0.0',
22
+ };
23
+ /**
24
+ * Supported schema versions for backward compatibility
25
+ */
26
+ export const SUPPORTED_VERSIONS = {
27
+ pattern: ['1.0.0'],
28
+ patternFile: ['1.0.0'],
29
+ historyFile: ['1.0.0'],
30
+ lockFile: ['1.0.0'],
31
+ variantsFile: ['1.0.0'],
32
+ config: ['1.0.0'],
33
+ };
34
+ /**
35
+ * Custom error class for schema validation failures
36
+ */
37
+ export class SchemaValidationError extends Error {
38
+ errors;
39
+ schemaType;
40
+ constructor(message, errors, schemaType) {
41
+ super(message);
42
+ this.errors = errors;
43
+ this.schemaType = schemaType;
44
+ this.name = 'SchemaValidationError';
45
+ }
46
+ /**
47
+ * Format errors as a human-readable string
48
+ */
49
+ formatErrors() {
50
+ return this.errors
51
+ .map((e) => ` - ${e.path}: ${e.message}`)
52
+ .join('\n');
53
+ }
54
+ }
55
+ // ============================================================================
56
+ // Valid Value Sets
57
+ // ============================================================================
58
+ const VALID_PATTERN_CATEGORIES = [
59
+ 'structural',
60
+ 'components',
61
+ 'styling',
62
+ 'api',
63
+ 'auth',
64
+ 'errors',
65
+ 'data-access',
66
+ 'testing',
67
+ 'logging',
68
+ 'security',
69
+ 'config',
70
+ 'types',
71
+ 'performance',
72
+ 'accessibility',
73
+ 'documentation',
74
+ ];
75
+ const VALID_PATTERN_STATUSES = ['discovered', 'approved', 'ignored'];
76
+ const VALID_SEVERITIES = ['error', 'warning', 'info', 'hint'];
77
+ const VALID_CONFIDENCE_LEVELS = ['high', 'medium', 'low', 'uncertain'];
78
+ const VALID_DETECTOR_TYPES = ['ast', 'regex', 'semantic', 'structural', 'custom'];
79
+ const VALID_VARIANT_SCOPES = ['global', 'directory', 'file'];
80
+ const VALID_HISTORY_EVENT_TYPES = [
81
+ 'created',
82
+ 'approved',
83
+ 'ignored',
84
+ 'updated',
85
+ 'deleted',
86
+ 'confidence_changed',
87
+ 'locations_changed',
88
+ 'severity_changed',
89
+ ];
90
+ const VALID_AI_PROVIDERS = ['openai', 'anthropic', 'ollama'];
91
+ const VALID_CI_FAIL_ON = ['error', 'warning', 'none'];
92
+ const VALID_REPORT_FORMATS = ['json', 'text', 'github', 'gitlab'];
93
+ // ============================================================================
94
+ // Helper Validation Functions
95
+ // ============================================================================
96
+ /**
97
+ * Check if a value is a non-null object
98
+ */
99
+ function isObject(value) {
100
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
101
+ }
102
+ /**
103
+ * Get a property from an object safely (for use with index signatures)
104
+ */
105
+ function get(obj, key) {
106
+ return obj[key];
107
+ }
108
+ /**
109
+ * Check if a value is a non-empty string
110
+ */
111
+ function isNonEmptyString(value) {
112
+ return typeof value === 'string' && value.length > 0;
113
+ }
114
+ /**
115
+ * Check if a value is a valid ISO date string
116
+ */
117
+ function isISODateString(value) {
118
+ if (typeof value !== 'string')
119
+ return false;
120
+ const date = new Date(value);
121
+ return !isNaN(date.getTime()) && value.includes('T');
122
+ }
123
+ /**
124
+ * Check if a value is a number within a range
125
+ */
126
+ function isNumberInRange(value, min, max) {
127
+ return typeof value === 'number' && !isNaN(value) && value >= min && value <= max;
128
+ }
129
+ /**
130
+ * Check if a value is a positive integer
131
+ */
132
+ function isPositiveInteger(value) {
133
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
134
+ }
135
+ /**
136
+ * Check if a value is in a valid set
137
+ */
138
+ function isOneOf(value, validValues) {
139
+ return validValues.includes(value);
140
+ }
141
+ /**
142
+ * Check if a value is a valid semver version string
143
+ */
144
+ function isSemverVersion(value) {
145
+ if (typeof value !== 'string')
146
+ return false;
147
+ return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/.test(value);
148
+ }
149
+ // ============================================================================
150
+ // Component Validators
151
+ // ============================================================================
152
+ /**
153
+ * Validate a PatternLocation object
154
+ */
155
+ function validatePatternLocation(location, path, errors) {
156
+ if (!isObject(location)) {
157
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof location });
158
+ return false;
159
+ }
160
+ let valid = true;
161
+ const file = get(location, 'file');
162
+ const line = get(location, 'line');
163
+ const column = get(location, 'column');
164
+ const endLine = get(location, 'endLine');
165
+ const endColumn = get(location, 'endColumn');
166
+ if (!isNonEmptyString(file)) {
167
+ errors.push({ path: `${path}.file`, message: 'Must be a non-empty string', expected: 'string', actual: file });
168
+ valid = false;
169
+ }
170
+ if (!isPositiveInteger(line) || line < 1) {
171
+ errors.push({ path: `${path}.line`, message: 'Must be a positive integer >= 1', expected: 'number >= 1', actual: line });
172
+ valid = false;
173
+ }
174
+ if (!isPositiveInteger(column) || column < 1) {
175
+ errors.push({ path: `${path}.column`, message: 'Must be a positive integer >= 1', expected: 'number >= 1', actual: column });
176
+ valid = false;
177
+ }
178
+ // Optional fields
179
+ if (endLine !== undefined && (!isPositiveInteger(endLine) || endLine < 1)) {
180
+ errors.push({ path: `${path}.endLine`, message: 'Must be a positive integer >= 1', expected: 'number >= 1', actual: endLine });
181
+ valid = false;
182
+ }
183
+ if (endColumn !== undefined && (!isPositiveInteger(endColumn) || endColumn < 1)) {
184
+ errors.push({ path: `${path}.endColumn`, message: 'Must be a positive integer >= 1', expected: 'number >= 1', actual: endColumn });
185
+ valid = false;
186
+ }
187
+ return valid;
188
+ }
189
+ /**
190
+ * Validate an OutlierLocation object
191
+ */
192
+ function validateOutlierLocation(location, path, errors) {
193
+ if (!validatePatternLocation(location, path, errors)) {
194
+ return false;
195
+ }
196
+ const loc = location;
197
+ let valid = true;
198
+ const reason = get(loc, 'reason');
199
+ const deviationScore = get(loc, 'deviationScore');
200
+ if (!isNonEmptyString(reason)) {
201
+ errors.push({ path: `${path}.reason`, message: 'Must be a non-empty string', expected: 'string', actual: reason });
202
+ valid = false;
203
+ }
204
+ if (deviationScore !== undefined && !isNumberInRange(deviationScore, 0, 1)) {
205
+ errors.push({ path: `${path}.deviationScore`, message: 'Must be a number between 0 and 1', expected: '0-1', actual: deviationScore });
206
+ valid = false;
207
+ }
208
+ return valid;
209
+ }
210
+ /**
211
+ * Validate a ConfidenceInfo object
212
+ */
213
+ function validateConfidenceInfo(confidence, path, errors) {
214
+ if (!isObject(confidence)) {
215
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof confidence });
216
+ return false;
217
+ }
218
+ let valid = true;
219
+ const frequency = get(confidence, 'frequency');
220
+ const consistency = get(confidence, 'consistency');
221
+ const age = get(confidence, 'age');
222
+ const spread = get(confidence, 'spread');
223
+ const score = get(confidence, 'score');
224
+ const level = get(confidence, 'level');
225
+ if (!isNumberInRange(frequency, 0, 1)) {
226
+ errors.push({ path: `${path}.frequency`, message: 'Must be a number between 0 and 1', expected: '0-1', actual: frequency });
227
+ valid = false;
228
+ }
229
+ if (!isNumberInRange(consistency, 0, 1)) {
230
+ errors.push({ path: `${path}.consistency`, message: 'Must be a number between 0 and 1', expected: '0-1', actual: consistency });
231
+ valid = false;
232
+ }
233
+ if (!isPositiveInteger(age) && age !== 0) {
234
+ errors.push({ path: `${path}.age`, message: 'Must be a non-negative number', expected: 'number >= 0', actual: age });
235
+ valid = false;
236
+ }
237
+ if (!isPositiveInteger(spread)) {
238
+ errors.push({ path: `${path}.spread`, message: 'Must be a non-negative integer', expected: 'integer >= 0', actual: spread });
239
+ valid = false;
240
+ }
241
+ if (!isNumberInRange(score, 0, 1)) {
242
+ errors.push({ path: `${path}.score`, message: 'Must be a number between 0 and 1', expected: '0-1', actual: score });
243
+ valid = false;
244
+ }
245
+ if (!isOneOf(level, VALID_CONFIDENCE_LEVELS)) {
246
+ errors.push({ path: `${path}.level`, message: `Must be one of: ${VALID_CONFIDENCE_LEVELS.join(', ')}`, expected: VALID_CONFIDENCE_LEVELS.join('|'), actual: level });
247
+ valid = false;
248
+ }
249
+ return valid;
250
+ }
251
+ /**
252
+ * Validate a DetectorConfig object
253
+ */
254
+ function validateDetectorConfig(detector, path, errors) {
255
+ if (!isObject(detector)) {
256
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof detector });
257
+ return false;
258
+ }
259
+ let valid = true;
260
+ const type = get(detector, 'type');
261
+ const config = get(detector, 'config');
262
+ const ast = get(detector, 'ast');
263
+ const regex = get(detector, 'regex');
264
+ const structural = get(detector, 'structural');
265
+ const custom = get(detector, 'custom');
266
+ if (!isOneOf(type, VALID_DETECTOR_TYPES)) {
267
+ errors.push({ path: `${path}.type`, message: `Must be one of: ${VALID_DETECTOR_TYPES.join(', ')}`, expected: VALID_DETECTOR_TYPES.join('|'), actual: type });
268
+ valid = false;
269
+ }
270
+ if (!isObject(config)) {
271
+ errors.push({ path: `${path}.config`, message: 'Must be an object', expected: 'object', actual: typeof config });
272
+ valid = false;
273
+ }
274
+ // Type-specific validation is optional - just ensure the objects exist if provided
275
+ if (ast !== undefined && !isObject(ast)) {
276
+ errors.push({ path: `${path}.ast`, message: 'Must be an object if provided', expected: 'object', actual: typeof ast });
277
+ valid = false;
278
+ }
279
+ if (regex !== undefined && !isObject(regex)) {
280
+ errors.push({ path: `${path}.regex`, message: 'Must be an object if provided', expected: 'object', actual: typeof regex });
281
+ valid = false;
282
+ }
283
+ if (structural !== undefined && !isObject(structural)) {
284
+ errors.push({ path: `${path}.structural`, message: 'Must be an object if provided', expected: 'object', actual: typeof structural });
285
+ valid = false;
286
+ }
287
+ if (custom !== undefined && !isObject(custom)) {
288
+ errors.push({ path: `${path}.custom`, message: 'Must be an object if provided', expected: 'object', actual: typeof custom });
289
+ valid = false;
290
+ }
291
+ return valid;
292
+ }
293
+ /**
294
+ * Validate a PatternMetadata object
295
+ */
296
+ function validatePatternMetadata(metadata, path, errors) {
297
+ if (!isObject(metadata)) {
298
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof metadata });
299
+ return false;
300
+ }
301
+ let valid = true;
302
+ const firstSeen = get(metadata, 'firstSeen');
303
+ const lastSeen = get(metadata, 'lastSeen');
304
+ const approvedAt = get(metadata, 'approvedAt');
305
+ const approvedBy = get(metadata, 'approvedBy');
306
+ const version = get(metadata, 'version');
307
+ const tags = get(metadata, 'tags');
308
+ const relatedPatterns = get(metadata, 'relatedPatterns');
309
+ const source = get(metadata, 'source');
310
+ const custom = get(metadata, 'custom');
311
+ if (!isISODateString(firstSeen)) {
312
+ errors.push({ path: `${path}.firstSeen`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: firstSeen });
313
+ valid = false;
314
+ }
315
+ if (!isISODateString(lastSeen)) {
316
+ errors.push({ path: `${path}.lastSeen`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: lastSeen });
317
+ valid = false;
318
+ }
319
+ // Optional fields
320
+ if (approvedAt !== undefined && !isISODateString(approvedAt)) {
321
+ errors.push({ path: `${path}.approvedAt`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: approvedAt });
322
+ valid = false;
323
+ }
324
+ if (approvedBy !== undefined && typeof approvedBy !== 'string') {
325
+ errors.push({ path: `${path}.approvedBy`, message: 'Must be a string', expected: 'string', actual: typeof approvedBy });
326
+ valid = false;
327
+ }
328
+ if (version !== undefined && typeof version !== 'string') {
329
+ errors.push({ path: `${path}.version`, message: 'Must be a string', expected: 'string', actual: typeof version });
330
+ valid = false;
331
+ }
332
+ if (tags !== undefined) {
333
+ if (!Array.isArray(tags)) {
334
+ errors.push({ path: `${path}.tags`, message: 'Must be an array', expected: 'array', actual: typeof tags });
335
+ valid = false;
336
+ }
337
+ else if (!tags.every((t) => typeof t === 'string')) {
338
+ errors.push({ path: `${path}.tags`, message: 'All tags must be strings', expected: 'string[]', actual: tags });
339
+ valid = false;
340
+ }
341
+ }
342
+ if (relatedPatterns !== undefined) {
343
+ if (!Array.isArray(relatedPatterns)) {
344
+ errors.push({ path: `${path}.relatedPatterns`, message: 'Must be an array', expected: 'array', actual: typeof relatedPatterns });
345
+ valid = false;
346
+ }
347
+ else if (!relatedPatterns.every((p) => typeof p === 'string')) {
348
+ errors.push({ path: `${path}.relatedPatterns`, message: 'All related patterns must be strings', expected: 'string[]', actual: relatedPatterns });
349
+ valid = false;
350
+ }
351
+ }
352
+ if (source !== undefined && typeof source !== 'string') {
353
+ errors.push({ path: `${path}.source`, message: 'Must be a string', expected: 'string', actual: typeof source });
354
+ valid = false;
355
+ }
356
+ if (custom !== undefined && !isObject(custom)) {
357
+ errors.push({ path: `${path}.custom`, message: 'Must be an object', expected: 'object', actual: typeof custom });
358
+ valid = false;
359
+ }
360
+ return valid;
361
+ }
362
+ // ============================================================================
363
+ // Pattern Validators
364
+ // ============================================================================
365
+ /**
366
+ * Validate a StoredPattern object
367
+ */
368
+ function validateStoredPattern(pattern, path, errors) {
369
+ if (!isObject(pattern)) {
370
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof pattern });
371
+ return false;
372
+ }
373
+ let valid = true;
374
+ const id = get(pattern, 'id');
375
+ const subcategory = get(pattern, 'subcategory');
376
+ const name = get(pattern, 'name');
377
+ const description = get(pattern, 'description');
378
+ const severity = get(pattern, 'severity');
379
+ const autoFixable = get(pattern, 'autoFixable');
380
+ const detector = get(pattern, 'detector');
381
+ const confidence = get(pattern, 'confidence');
382
+ const metadata = get(pattern, 'metadata');
383
+ const locations = get(pattern, 'locations');
384
+ const outliers = get(pattern, 'outliers');
385
+ // Required string fields
386
+ if (!isNonEmptyString(id)) {
387
+ errors.push({ path: `${path}.id`, message: 'Must be a non-empty string', expected: 'string', actual: id });
388
+ valid = false;
389
+ }
390
+ if (!isNonEmptyString(subcategory)) {
391
+ errors.push({ path: `${path}.subcategory`, message: 'Must be a non-empty string', expected: 'string', actual: subcategory });
392
+ valid = false;
393
+ }
394
+ if (!isNonEmptyString(name)) {
395
+ errors.push({ path: `${path}.name`, message: 'Must be a non-empty string', expected: 'string', actual: name });
396
+ valid = false;
397
+ }
398
+ if (!isNonEmptyString(description)) {
399
+ errors.push({ path: `${path}.description`, message: 'Must be a non-empty string', expected: 'string', actual: description });
400
+ valid = false;
401
+ }
402
+ // Enum fields
403
+ if (!isOneOf(severity, VALID_SEVERITIES)) {
404
+ errors.push({ path: `${path}.severity`, message: `Must be one of: ${VALID_SEVERITIES.join(', ')}`, expected: VALID_SEVERITIES.join('|'), actual: severity });
405
+ valid = false;
406
+ }
407
+ // Boolean field
408
+ if (typeof autoFixable !== 'boolean') {
409
+ errors.push({ path: `${path}.autoFixable`, message: 'Must be a boolean', expected: 'boolean', actual: typeof autoFixable });
410
+ valid = false;
411
+ }
412
+ // Complex fields
413
+ if (!validateDetectorConfig(detector, `${path}.detector`, errors)) {
414
+ valid = false;
415
+ }
416
+ if (!validateConfidenceInfo(confidence, `${path}.confidence`, errors)) {
417
+ valid = false;
418
+ }
419
+ if (!validatePatternMetadata(metadata, `${path}.metadata`, errors)) {
420
+ valid = false;
421
+ }
422
+ // Array fields
423
+ if (!Array.isArray(locations)) {
424
+ errors.push({ path: `${path}.locations`, message: 'Must be an array', expected: 'array', actual: typeof locations });
425
+ valid = false;
426
+ }
427
+ else {
428
+ locations.forEach((loc, i) => {
429
+ if (!validatePatternLocation(loc, `${path}.locations[${i}]`, errors)) {
430
+ valid = false;
431
+ }
432
+ });
433
+ }
434
+ if (!Array.isArray(outliers)) {
435
+ errors.push({ path: `${path}.outliers`, message: 'Must be an array', expected: 'array', actual: typeof outliers });
436
+ valid = false;
437
+ }
438
+ else {
439
+ outliers.forEach((loc, i) => {
440
+ if (!validateOutlierLocation(loc, `${path}.outliers[${i}]`, errors)) {
441
+ valid = false;
442
+ }
443
+ });
444
+ }
445
+ return valid;
446
+ }
447
+ /**
448
+ * Validate a full Pattern object (includes status and category)
449
+ */
450
+ function validatePattern(pattern, path, errors) {
451
+ if (!isObject(pattern)) {
452
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof pattern });
453
+ return false;
454
+ }
455
+ let valid = true;
456
+ const category = get(pattern, 'category');
457
+ const status = get(pattern, 'status');
458
+ // Validate category (not in StoredPattern)
459
+ if (!isOneOf(category, VALID_PATTERN_CATEGORIES)) {
460
+ errors.push({ path: `${path}.category`, message: `Must be one of: ${VALID_PATTERN_CATEGORIES.join(', ')}`, expected: VALID_PATTERN_CATEGORIES.join('|'), actual: category });
461
+ valid = false;
462
+ }
463
+ // Validate status (not in StoredPattern)
464
+ if (!isOneOf(status, VALID_PATTERN_STATUSES)) {
465
+ errors.push({ path: `${path}.status`, message: `Must be one of: ${VALID_PATTERN_STATUSES.join(', ')}`, expected: VALID_PATTERN_STATUSES.join('|'), actual: status });
466
+ valid = false;
467
+ }
468
+ // Validate the rest using StoredPattern validator
469
+ // Create a copy without category and status for StoredPattern validation
470
+ const storedPatternFields = { ...pattern };
471
+ delete storedPatternFields['category'];
472
+ delete storedPatternFields['status'];
473
+ if (!validateStoredPattern(storedPatternFields, path, errors)) {
474
+ valid = false;
475
+ }
476
+ return valid;
477
+ }
478
+ // ============================================================================
479
+ // File Validators
480
+ // ============================================================================
481
+ /**
482
+ * Validate a PatternFile object
483
+ *
484
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
485
+ */
486
+ export function validatePatternFile(data) {
487
+ const errors = [];
488
+ if (!isObject(data)) {
489
+ errors.push({ path: '', message: 'Pattern file must be an object', expected: 'object', actual: typeof data });
490
+ return { valid: false, errors };
491
+ }
492
+ const version = get(data, 'version');
493
+ const category = get(data, 'category');
494
+ const patterns = get(data, 'patterns');
495
+ const lastUpdated = get(data, 'lastUpdated');
496
+ const checksum = get(data, 'checksum');
497
+ // Version validation
498
+ if (!isSemverVersion(version)) {
499
+ errors.push({ path: 'version', message: 'Must be a valid semver version string', expected: 'semver', actual: version });
500
+ }
501
+ else if (!SUPPORTED_VERSIONS.patternFile.includes(version)) {
502
+ errors.push({ path: 'version', message: `Unsupported version. Supported: ${SUPPORTED_VERSIONS.patternFile.join(', ')}`, expected: SUPPORTED_VERSIONS.patternFile.join('|'), actual: version });
503
+ }
504
+ // Category validation
505
+ if (!isOneOf(category, VALID_PATTERN_CATEGORIES)) {
506
+ errors.push({ path: 'category', message: `Must be one of: ${VALID_PATTERN_CATEGORIES.join(', ')}`, expected: VALID_PATTERN_CATEGORIES.join('|'), actual: category });
507
+ }
508
+ // Patterns array validation
509
+ if (!Array.isArray(patterns)) {
510
+ errors.push({ path: 'patterns', message: 'Must be an array', expected: 'array', actual: typeof patterns });
511
+ }
512
+ else {
513
+ patterns.forEach((pattern, i) => {
514
+ validateStoredPattern(pattern, `patterns[${i}]`, errors);
515
+ });
516
+ }
517
+ // Last updated validation
518
+ if (!isISODateString(lastUpdated)) {
519
+ errors.push({ path: 'lastUpdated', message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: lastUpdated });
520
+ }
521
+ // Optional checksum
522
+ if (checksum !== undefined && typeof checksum !== 'string') {
523
+ errors.push({ path: 'checksum', message: 'Must be a string if provided', expected: 'string', actual: typeof checksum });
524
+ }
525
+ if (errors.length > 0) {
526
+ return { valid: false, errors };
527
+ }
528
+ return { valid: true, data: data };
529
+ }
530
+ /**
531
+ * Validate a PatternHistoryEvent object
532
+ */
533
+ function validatePatternHistoryEvent(event, path, errors) {
534
+ if (!isObject(event)) {
535
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof event });
536
+ return false;
537
+ }
538
+ let valid = true;
539
+ const timestamp = get(event, 'timestamp');
540
+ const type = get(event, 'type');
541
+ const patternId = get(event, 'patternId');
542
+ const user = get(event, 'user');
543
+ const details = get(event, 'details');
544
+ if (!isISODateString(timestamp)) {
545
+ errors.push({ path: `${path}.timestamp`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: timestamp });
546
+ valid = false;
547
+ }
548
+ if (!isOneOf(type, VALID_HISTORY_EVENT_TYPES)) {
549
+ errors.push({ path: `${path}.type`, message: `Must be one of: ${VALID_HISTORY_EVENT_TYPES.join(', ')}`, expected: VALID_HISTORY_EVENT_TYPES.join('|'), actual: type });
550
+ valid = false;
551
+ }
552
+ if (!isNonEmptyString(patternId)) {
553
+ errors.push({ path: `${path}.patternId`, message: 'Must be a non-empty string', expected: 'string', actual: patternId });
554
+ valid = false;
555
+ }
556
+ // Optional fields
557
+ if (user !== undefined && typeof user !== 'string') {
558
+ errors.push({ path: `${path}.user`, message: 'Must be a string if provided', expected: 'string', actual: typeof user });
559
+ valid = false;
560
+ }
561
+ if (details !== undefined && !isObject(details)) {
562
+ errors.push({ path: `${path}.details`, message: 'Must be an object if provided', expected: 'object', actual: typeof details });
563
+ valid = false;
564
+ }
565
+ return valid;
566
+ }
567
+ /**
568
+ * Validate a PatternHistory object
569
+ */
570
+ function validatePatternHistory(history, path, errors) {
571
+ if (!isObject(history)) {
572
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof history });
573
+ return false;
574
+ }
575
+ let valid = true;
576
+ const patternId = get(history, 'patternId');
577
+ const category = get(history, 'category');
578
+ const events = get(history, 'events');
579
+ const createdAt = get(history, 'createdAt');
580
+ const lastModified = get(history, 'lastModified');
581
+ if (!isNonEmptyString(patternId)) {
582
+ errors.push({ path: `${path}.patternId`, message: 'Must be a non-empty string', expected: 'string', actual: patternId });
583
+ valid = false;
584
+ }
585
+ if (!isOneOf(category, VALID_PATTERN_CATEGORIES)) {
586
+ errors.push({ path: `${path}.category`, message: `Must be one of: ${VALID_PATTERN_CATEGORIES.join(', ')}`, expected: VALID_PATTERN_CATEGORIES.join('|'), actual: category });
587
+ valid = false;
588
+ }
589
+ if (!Array.isArray(events)) {
590
+ errors.push({ path: `${path}.events`, message: 'Must be an array', expected: 'array', actual: typeof events });
591
+ valid = false;
592
+ }
593
+ else {
594
+ events.forEach((event, i) => {
595
+ if (!validatePatternHistoryEvent(event, `${path}.events[${i}]`, errors)) {
596
+ valid = false;
597
+ }
598
+ });
599
+ }
600
+ if (!isISODateString(createdAt)) {
601
+ errors.push({ path: `${path}.createdAt`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: createdAt });
602
+ valid = false;
603
+ }
604
+ if (!isISODateString(lastModified)) {
605
+ errors.push({ path: `${path}.lastModified`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: lastModified });
606
+ valid = false;
607
+ }
608
+ return valid;
609
+ }
610
+ /**
611
+ * Validate a HistoryFile object
612
+ *
613
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
614
+ */
615
+ export function validateHistoryFile(data) {
616
+ const errors = [];
617
+ if (!isObject(data)) {
618
+ errors.push({ path: '', message: 'History file must be an object', expected: 'object', actual: typeof data });
619
+ return { valid: false, errors };
620
+ }
621
+ const version = get(data, 'version');
622
+ const patterns = get(data, 'patterns');
623
+ const lastUpdated = get(data, 'lastUpdated');
624
+ // Version validation
625
+ if (!isSemverVersion(version)) {
626
+ errors.push({ path: 'version', message: 'Must be a valid semver version string', expected: 'semver', actual: version });
627
+ }
628
+ else if (!SUPPORTED_VERSIONS.historyFile.includes(version)) {
629
+ errors.push({ path: 'version', message: `Unsupported version. Supported: ${SUPPORTED_VERSIONS.historyFile.join(', ')}`, expected: SUPPORTED_VERSIONS.historyFile.join('|'), actual: version });
630
+ }
631
+ // Patterns array validation
632
+ if (!Array.isArray(patterns)) {
633
+ errors.push({ path: 'patterns', message: 'Must be an array', expected: 'array', actual: typeof patterns });
634
+ }
635
+ else {
636
+ patterns.forEach((history, i) => {
637
+ validatePatternHistory(history, `patterns[${i}]`, errors);
638
+ });
639
+ }
640
+ // Last updated validation
641
+ if (!isISODateString(lastUpdated)) {
642
+ errors.push({ path: 'lastUpdated', message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: lastUpdated });
643
+ }
644
+ if (errors.length > 0) {
645
+ return { valid: false, errors };
646
+ }
647
+ return { valid: true, data: data };
648
+ }
649
+ /**
650
+ * Validate a LockedPattern object
651
+ */
652
+ function validateLockedPattern(pattern, path, errors) {
653
+ if (!isObject(pattern)) {
654
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof pattern });
655
+ return false;
656
+ }
657
+ let valid = true;
658
+ const id = get(pattern, 'id');
659
+ const category = get(pattern, 'category');
660
+ const name = get(pattern, 'name');
661
+ const confidenceScore = get(pattern, 'confidenceScore');
662
+ const severity = get(pattern, 'severity');
663
+ const definitionHash = get(pattern, 'definitionHash');
664
+ const lockedAt = get(pattern, 'lockedAt');
665
+ if (!isNonEmptyString(id)) {
666
+ errors.push({ path: `${path}.id`, message: 'Must be a non-empty string', expected: 'string', actual: id });
667
+ valid = false;
668
+ }
669
+ if (!isOneOf(category, VALID_PATTERN_CATEGORIES)) {
670
+ errors.push({ path: `${path}.category`, message: `Must be one of: ${VALID_PATTERN_CATEGORIES.join(', ')}`, expected: VALID_PATTERN_CATEGORIES.join('|'), actual: category });
671
+ valid = false;
672
+ }
673
+ if (!isNonEmptyString(name)) {
674
+ errors.push({ path: `${path}.name`, message: 'Must be a non-empty string', expected: 'string', actual: name });
675
+ valid = false;
676
+ }
677
+ if (!isNumberInRange(confidenceScore, 0, 1)) {
678
+ errors.push({ path: `${path}.confidenceScore`, message: 'Must be a number between 0 and 1', expected: '0-1', actual: confidenceScore });
679
+ valid = false;
680
+ }
681
+ if (!isOneOf(severity, VALID_SEVERITIES)) {
682
+ errors.push({ path: `${path}.severity`, message: `Must be one of: ${VALID_SEVERITIES.join(', ')}`, expected: VALID_SEVERITIES.join('|'), actual: severity });
683
+ valid = false;
684
+ }
685
+ if (!isNonEmptyString(definitionHash)) {
686
+ errors.push({ path: `${path}.definitionHash`, message: 'Must be a non-empty string', expected: 'string', actual: definitionHash });
687
+ valid = false;
688
+ }
689
+ if (!isISODateString(lockedAt)) {
690
+ errors.push({ path: `${path}.lockedAt`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: lockedAt });
691
+ valid = false;
692
+ }
693
+ return valid;
694
+ }
695
+ /**
696
+ * Validate a LockFile object
697
+ *
698
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
699
+ */
700
+ export function validateLockFile(data) {
701
+ const errors = [];
702
+ if (!isObject(data)) {
703
+ errors.push({ path: '', message: 'Lock file must be an object', expected: 'object', actual: typeof data });
704
+ return { valid: false, errors };
705
+ }
706
+ const version = get(data, 'version');
707
+ const patterns = get(data, 'patterns');
708
+ const generatedAt = get(data, 'generatedAt');
709
+ const checksum = get(data, 'checksum');
710
+ // Version validation
711
+ if (!isSemverVersion(version)) {
712
+ errors.push({ path: 'version', message: 'Must be a valid semver version string', expected: 'semver', actual: version });
713
+ }
714
+ else if (!SUPPORTED_VERSIONS.lockFile.includes(version)) {
715
+ errors.push({ path: 'version', message: `Unsupported version. Supported: ${SUPPORTED_VERSIONS.lockFile.join(', ')}`, expected: SUPPORTED_VERSIONS.lockFile.join('|'), actual: version });
716
+ }
717
+ // Patterns array validation
718
+ if (!Array.isArray(patterns)) {
719
+ errors.push({ path: 'patterns', message: 'Must be an array', expected: 'array', actual: typeof patterns });
720
+ }
721
+ else {
722
+ patterns.forEach((pattern, i) => {
723
+ validateLockedPattern(pattern, `patterns[${i}]`, errors);
724
+ });
725
+ }
726
+ // Generated at validation
727
+ if (!isISODateString(generatedAt)) {
728
+ errors.push({ path: 'generatedAt', message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: generatedAt });
729
+ }
730
+ // Checksum validation
731
+ if (!isNonEmptyString(checksum)) {
732
+ errors.push({ path: 'checksum', message: 'Must be a non-empty string', expected: 'string', actual: checksum });
733
+ }
734
+ if (errors.length > 0) {
735
+ return { valid: false, errors };
736
+ }
737
+ return { valid: true, data: data };
738
+ }
739
+ /**
740
+ * Validate a PatternVariant object
741
+ */
742
+ function validatePatternVariant(variant, path, errors) {
743
+ if (!isObject(variant)) {
744
+ errors.push({ path, message: 'Must be an object', expected: 'object', actual: typeof variant });
745
+ return false;
746
+ }
747
+ let valid = true;
748
+ const id = get(variant, 'id');
749
+ const patternId = get(variant, 'patternId');
750
+ const name = get(variant, 'name');
751
+ const reason = get(variant, 'reason');
752
+ const scope = get(variant, 'scope');
753
+ const scopeValue = get(variant, 'scopeValue');
754
+ const locations = get(variant, 'locations');
755
+ const createdAt = get(variant, 'createdAt');
756
+ const createdBy = get(variant, 'createdBy');
757
+ const active = get(variant, 'active');
758
+ if (!isNonEmptyString(id)) {
759
+ errors.push({ path: `${path}.id`, message: 'Must be a non-empty string', expected: 'string', actual: id });
760
+ valid = false;
761
+ }
762
+ if (!isNonEmptyString(patternId)) {
763
+ errors.push({ path: `${path}.patternId`, message: 'Must be a non-empty string', expected: 'string', actual: patternId });
764
+ valid = false;
765
+ }
766
+ if (!isNonEmptyString(name)) {
767
+ errors.push({ path: `${path}.name`, message: 'Must be a non-empty string', expected: 'string', actual: name });
768
+ valid = false;
769
+ }
770
+ if (!isNonEmptyString(reason)) {
771
+ errors.push({ path: `${path}.reason`, message: 'Must be a non-empty string', expected: 'string', actual: reason });
772
+ valid = false;
773
+ }
774
+ if (!isOneOf(scope, VALID_VARIANT_SCOPES)) {
775
+ errors.push({ path: `${path}.scope`, message: `Must be one of: ${VALID_VARIANT_SCOPES.join(', ')}`, expected: VALID_VARIANT_SCOPES.join('|'), actual: scope });
776
+ valid = false;
777
+ }
778
+ // scopeValue is optional but must be string if provided
779
+ if (scopeValue !== undefined && typeof scopeValue !== 'string') {
780
+ errors.push({ path: `${path}.scopeValue`, message: 'Must be a string if provided', expected: 'string', actual: typeof scopeValue });
781
+ valid = false;
782
+ }
783
+ if (!Array.isArray(locations)) {
784
+ errors.push({ path: `${path}.locations`, message: 'Must be an array', expected: 'array', actual: typeof locations });
785
+ valid = false;
786
+ }
787
+ else {
788
+ locations.forEach((loc, i) => {
789
+ if (!validatePatternLocation(loc, `${path}.locations[${i}]`, errors)) {
790
+ valid = false;
791
+ }
792
+ });
793
+ }
794
+ if (!isISODateString(createdAt)) {
795
+ errors.push({ path: `${path}.createdAt`, message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: createdAt });
796
+ valid = false;
797
+ }
798
+ if (createdBy !== undefined && typeof createdBy !== 'string') {
799
+ errors.push({ path: `${path}.createdBy`, message: 'Must be a string if provided', expected: 'string', actual: typeof createdBy });
800
+ valid = false;
801
+ }
802
+ if (typeof active !== 'boolean') {
803
+ errors.push({ path: `${path}.active`, message: 'Must be a boolean', expected: 'boolean', actual: typeof active });
804
+ valid = false;
805
+ }
806
+ return valid;
807
+ }
808
+ /**
809
+ * Validate a VariantsFile object
810
+ *
811
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
812
+ */
813
+ export function validateVariantsFile(data) {
814
+ const errors = [];
815
+ if (!isObject(data)) {
816
+ errors.push({ path: '', message: 'Variants file must be an object', expected: 'object', actual: typeof data });
817
+ return { valid: false, errors };
818
+ }
819
+ const version = get(data, 'version');
820
+ const variants = get(data, 'variants');
821
+ const lastUpdated = get(data, 'lastUpdated');
822
+ // Version validation
823
+ if (!isSemverVersion(version)) {
824
+ errors.push({ path: 'version', message: 'Must be a valid semver version string', expected: 'semver', actual: version });
825
+ }
826
+ else if (!SUPPORTED_VERSIONS.variantsFile.includes(version)) {
827
+ errors.push({ path: 'version', message: `Unsupported version. Supported: ${SUPPORTED_VERSIONS.variantsFile.join(', ')}`, expected: SUPPORTED_VERSIONS.variantsFile.join('|'), actual: version });
828
+ }
829
+ // Variants array validation
830
+ if (!Array.isArray(variants)) {
831
+ errors.push({ path: 'variants', message: 'Must be an array', expected: 'array', actual: typeof variants });
832
+ }
833
+ else {
834
+ variants.forEach((variant, i) => {
835
+ validatePatternVariant(variant, `variants[${i}]`, errors);
836
+ });
837
+ }
838
+ // Last updated validation
839
+ if (!isISODateString(lastUpdated)) {
840
+ errors.push({ path: 'lastUpdated', message: 'Must be a valid ISO date string', expected: 'ISO date string', actual: lastUpdated });
841
+ }
842
+ if (errors.length > 0) {
843
+ return { valid: false, errors };
844
+ }
845
+ return { valid: true, data: data };
846
+ }
847
+ // ============================================================================
848
+ // Config Validator
849
+ // ============================================================================
850
+ /**
851
+ * Validate a DriftConfig object
852
+ *
853
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
854
+ */
855
+ export function validateConfig(data) {
856
+ const errors = [];
857
+ if (!isObject(data)) {
858
+ errors.push({ path: '', message: 'Config must be an object', expected: 'object', actual: typeof data });
859
+ return { valid: false, errors };
860
+ }
861
+ const severity = get(data, 'severity');
862
+ const ignore = get(data, 'ignore');
863
+ const ai = get(data, 'ai');
864
+ const ci = get(data, 'ci');
865
+ const learning = get(data, 'learning');
866
+ const performance = get(data, 'performance');
867
+ // Severity overrides (optional)
868
+ if (severity !== undefined) {
869
+ if (!isObject(severity)) {
870
+ errors.push({ path: 'severity', message: 'Must be an object', expected: 'object', actual: typeof severity });
871
+ }
872
+ else {
873
+ Object.entries(severity).forEach(([key, value]) => {
874
+ if (!isOneOf(value, VALID_SEVERITIES)) {
875
+ errors.push({ path: `severity.${key}`, message: `Must be one of: ${VALID_SEVERITIES.join(', ')}`, expected: VALID_SEVERITIES.join('|'), actual: value });
876
+ }
877
+ });
878
+ }
879
+ }
880
+ // Ignore patterns (optional)
881
+ if (ignore !== undefined) {
882
+ if (!Array.isArray(ignore)) {
883
+ errors.push({ path: 'ignore', message: 'Must be an array', expected: 'array', actual: typeof ignore });
884
+ }
885
+ else if (!ignore.every((p) => typeof p === 'string')) {
886
+ errors.push({ path: 'ignore', message: 'All ignore patterns must be strings', expected: 'string[]', actual: ignore });
887
+ }
888
+ }
889
+ // AI config (optional)
890
+ if (ai !== undefined) {
891
+ if (!isObject(ai)) {
892
+ errors.push({ path: 'ai', message: 'Must be an object', expected: 'object', actual: typeof ai });
893
+ }
894
+ else {
895
+ const aiProvider = get(ai, 'provider');
896
+ const aiModel = get(ai, 'model');
897
+ if (!isOneOf(aiProvider, VALID_AI_PROVIDERS)) {
898
+ errors.push({ path: 'ai.provider', message: `Must be one of: ${VALID_AI_PROVIDERS.join(', ')}`, expected: VALID_AI_PROVIDERS.join('|'), actual: aiProvider });
899
+ }
900
+ if (aiModel !== undefined && typeof aiModel !== 'string') {
901
+ errors.push({ path: 'ai.model', message: 'Must be a string if provided', expected: 'string', actual: typeof aiModel });
902
+ }
903
+ }
904
+ }
905
+ // CI config (optional)
906
+ if (ci !== undefined) {
907
+ if (!isObject(ci)) {
908
+ errors.push({ path: 'ci', message: 'Must be an object', expected: 'object', actual: typeof ci });
909
+ }
910
+ else {
911
+ const ciFailOn = get(ci, 'failOn');
912
+ const ciReportFormat = get(ci, 'reportFormat');
913
+ if (!isOneOf(ciFailOn, VALID_CI_FAIL_ON)) {
914
+ errors.push({ path: 'ci.failOn', message: `Must be one of: ${VALID_CI_FAIL_ON.join(', ')}`, expected: VALID_CI_FAIL_ON.join('|'), actual: ciFailOn });
915
+ }
916
+ if (!isOneOf(ciReportFormat, VALID_REPORT_FORMATS)) {
917
+ errors.push({ path: 'ci.reportFormat', message: `Must be one of: ${VALID_REPORT_FORMATS.join(', ')}`, expected: VALID_REPORT_FORMATS.join('|'), actual: ciReportFormat });
918
+ }
919
+ }
920
+ }
921
+ // Learning config (optional)
922
+ if (learning !== undefined) {
923
+ if (!isObject(learning)) {
924
+ errors.push({ path: 'learning', message: 'Must be an object', expected: 'object', actual: typeof learning });
925
+ }
926
+ else {
927
+ const autoApproveThreshold = get(learning, 'autoApproveThreshold');
928
+ const minOccurrences = get(learning, 'minOccurrences');
929
+ if (!isNumberInRange(autoApproveThreshold, 0, 1)) {
930
+ errors.push({ path: 'learning.autoApproveThreshold', message: 'Must be a number between 0 and 1', expected: '0-1', actual: autoApproveThreshold });
931
+ }
932
+ if (!isPositiveInteger(minOccurrences)) {
933
+ errors.push({ path: 'learning.minOccurrences', message: 'Must be a non-negative integer', expected: 'integer >= 0', actual: minOccurrences });
934
+ }
935
+ }
936
+ }
937
+ // Performance config (optional)
938
+ if (performance !== undefined) {
939
+ if (!isObject(performance)) {
940
+ errors.push({ path: 'performance', message: 'Must be an object', expected: 'object', actual: typeof performance });
941
+ }
942
+ else {
943
+ const maxWorkers = get(performance, 'maxWorkers');
944
+ const cacheEnabled = get(performance, 'cacheEnabled');
945
+ const incrementalAnalysis = get(performance, 'incrementalAnalysis');
946
+ if (!isPositiveInteger(maxWorkers) || maxWorkers < 1) {
947
+ errors.push({ path: 'performance.maxWorkers', message: 'Must be a positive integer', expected: 'integer >= 1', actual: maxWorkers });
948
+ }
949
+ if (typeof cacheEnabled !== 'boolean') {
950
+ errors.push({ path: 'performance.cacheEnabled', message: 'Must be a boolean', expected: 'boolean', actual: typeof cacheEnabled });
951
+ }
952
+ if (typeof incrementalAnalysis !== 'boolean') {
953
+ errors.push({ path: 'performance.incrementalAnalysis', message: 'Must be a boolean', expected: 'boolean', actual: typeof incrementalAnalysis });
954
+ }
955
+ }
956
+ }
957
+ if (errors.length > 0) {
958
+ return { valid: false, errors };
959
+ }
960
+ return { valid: true, data: data };
961
+ }
962
+ // ============================================================================
963
+ // Single Pattern Validator (Public API)
964
+ // ============================================================================
965
+ /**
966
+ * Validate a single Pattern object
967
+ *
968
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
969
+ */
970
+ export function validateSinglePattern(data) {
971
+ const errors = [];
972
+ if (!validatePattern(data, '', errors)) {
973
+ return { valid: false, errors };
974
+ }
975
+ return { valid: true, data: data };
976
+ }
977
+ /**
978
+ * Validate a single StoredPattern object
979
+ *
980
+ * @requirements 4.5 - Pattern schema SHALL be validated on load/save
981
+ */
982
+ export function validateSingleStoredPattern(data) {
983
+ const errors = [];
984
+ if (!validateStoredPattern(data, '', errors)) {
985
+ return { valid: false, errors };
986
+ }
987
+ return { valid: true, data: data };
988
+ }
989
+ // ============================================================================
990
+ // Convenience Functions
991
+ // ============================================================================
992
+ /**
993
+ * Validate and throw if invalid
994
+ *
995
+ * @throws SchemaValidationError if validation fails
996
+ */
997
+ export function assertValidPatternFile(data) {
998
+ const result = validatePatternFile(data);
999
+ if (!result.valid) {
1000
+ throw new SchemaValidationError(`Invalid pattern file: ${result.errors.length} validation error(s)`, result.errors, 'PatternFile');
1001
+ }
1002
+ return result.data;
1003
+ }
1004
+ /**
1005
+ * Validate and throw if invalid
1006
+ *
1007
+ * @throws SchemaValidationError if validation fails
1008
+ */
1009
+ export function assertValidHistoryFile(data) {
1010
+ const result = validateHistoryFile(data);
1011
+ if (!result.valid) {
1012
+ throw new SchemaValidationError(`Invalid history file: ${result.errors.length} validation error(s)`, result.errors, 'HistoryFile');
1013
+ }
1014
+ return result.data;
1015
+ }
1016
+ /**
1017
+ * Validate and throw if invalid
1018
+ *
1019
+ * @throws SchemaValidationError if validation fails
1020
+ */
1021
+ export function assertValidLockFile(data) {
1022
+ const result = validateLockFile(data);
1023
+ if (!result.valid) {
1024
+ throw new SchemaValidationError(`Invalid lock file: ${result.errors.length} validation error(s)`, result.errors, 'LockFile');
1025
+ }
1026
+ return result.data;
1027
+ }
1028
+ /**
1029
+ * Validate and throw if invalid
1030
+ *
1031
+ * @throws SchemaValidationError if validation fails
1032
+ */
1033
+ export function assertValidVariantsFile(data) {
1034
+ const result = validateVariantsFile(data);
1035
+ if (!result.valid) {
1036
+ throw new SchemaValidationError(`Invalid variants file: ${result.errors.length} validation error(s)`, result.errors, 'VariantsFile');
1037
+ }
1038
+ return result.data;
1039
+ }
1040
+ /**
1041
+ * Validate and throw if invalid
1042
+ *
1043
+ * @throws SchemaValidationError if validation fails
1044
+ */
1045
+ export function assertValidConfig(data) {
1046
+ const result = validateConfig(data);
1047
+ if (!result.valid) {
1048
+ throw new SchemaValidationError(`Invalid config: ${result.errors.length} validation error(s)`, result.errors, 'DriftConfig');
1049
+ }
1050
+ return result.data;
1051
+ }
1052
+ /**
1053
+ * Validate and throw if invalid
1054
+ *
1055
+ * @throws SchemaValidationError if validation fails
1056
+ */
1057
+ export function assertValidPattern(data) {
1058
+ const result = validateSinglePattern(data);
1059
+ if (!result.valid) {
1060
+ throw new SchemaValidationError(`Invalid pattern: ${result.errors.length} validation error(s)`, result.errors, 'Pattern');
1061
+ }
1062
+ return result.data;
1063
+ }
1064
+ // ============================================================================
1065
+ // Version Checking
1066
+ // ============================================================================
1067
+ /**
1068
+ * Check if a schema version is supported
1069
+ */
1070
+ export function isVersionSupported(schemaType, version) {
1071
+ return SUPPORTED_VERSIONS[schemaType].includes(version);
1072
+ }
1073
+ /**
1074
+ * Get the current schema version for a type
1075
+ */
1076
+ export function getCurrentVersion(schemaType) {
1077
+ return SCHEMA_VERSIONS[schemaType];
1078
+ }
1079
+ /**
1080
+ * Format validation errors as a human-readable string
1081
+ */
1082
+ export function formatValidationErrors(errors) {
1083
+ if (errors.length === 0)
1084
+ return 'No errors';
1085
+ return errors
1086
+ .map((e) => {
1087
+ let msg = e.path ? `${e.path}: ${e.message}` : e.message;
1088
+ if (e.expected)
1089
+ msg += ` (expected: ${e.expected})`;
1090
+ if (e.actual !== undefined)
1091
+ msg += ` (got: ${JSON.stringify(e.actual)})`;
1092
+ return ` - ${msg}`;
1093
+ })
1094
+ .join('\n');
1095
+ }
1096
+ //# sourceMappingURL=schema-validator.js.map