@dependabit/monitor 0.1.1

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 (69) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/checkers/github-repo.d.ts +17 -0
  5. package/dist/checkers/github-repo.d.ts.map +1 -0
  6. package/dist/checkers/github-repo.js +115 -0
  7. package/dist/checkers/github-repo.js.map +1 -0
  8. package/dist/checkers/index.d.ts +7 -0
  9. package/dist/checkers/index.d.ts.map +1 -0
  10. package/dist/checkers/index.js +7 -0
  11. package/dist/checkers/index.js.map +1 -0
  12. package/dist/checkers/openapi.d.ts +24 -0
  13. package/dist/checkers/openapi.d.ts.map +1 -0
  14. package/dist/checkers/openapi.js +221 -0
  15. package/dist/checkers/openapi.js.map +1 -0
  16. package/dist/checkers/url-content.d.ts +16 -0
  17. package/dist/checkers/url-content.d.ts.map +1 -0
  18. package/dist/checkers/url-content.js +66 -0
  19. package/dist/checkers/url-content.js.map +1 -0
  20. package/dist/comparator.d.ts +16 -0
  21. package/dist/comparator.d.ts.map +1 -0
  22. package/dist/comparator.js +53 -0
  23. package/dist/comparator.js.map +1 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +12 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/monitor.d.ts +43 -0
  29. package/dist/monitor.d.ts.map +1 -0
  30. package/dist/monitor.js +85 -0
  31. package/dist/monitor.js.map +1 -0
  32. package/dist/normalizer.d.ts +24 -0
  33. package/dist/normalizer.d.ts.map +1 -0
  34. package/dist/normalizer.js +97 -0
  35. package/dist/normalizer.js.map +1 -0
  36. package/dist/scheduler.d.ts +64 -0
  37. package/dist/scheduler.d.ts.map +1 -0
  38. package/dist/scheduler.js +132 -0
  39. package/dist/scheduler.js.map +1 -0
  40. package/dist/severity.d.ts +22 -0
  41. package/dist/severity.d.ts.map +1 -0
  42. package/dist/severity.js +87 -0
  43. package/dist/severity.js.map +1 -0
  44. package/dist/types.d.ts +36 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +5 -0
  47. package/dist/types.js.map +1 -0
  48. package/package.json +39 -0
  49. package/src/checkers/github-repo.ts +150 -0
  50. package/src/checkers/index.ts +7 -0
  51. package/src/checkers/openapi.ts +310 -0
  52. package/src/checkers/url-content.ts +78 -0
  53. package/src/comparator.ts +68 -0
  54. package/src/index.ts +20 -0
  55. package/src/monitor.ts +120 -0
  56. package/src/normalizer.ts +122 -0
  57. package/src/scheduler.ts +175 -0
  58. package/src/severity.ts +112 -0
  59. package/src/types.ts +40 -0
  60. package/test/checkers/github-repo.test.ts +124 -0
  61. package/test/checkers/openapi.test.ts +352 -0
  62. package/test/checkers/url-content.test.ts +99 -0
  63. package/test/comparator.test.ts +108 -0
  64. package/test/monitor.test.ts +177 -0
  65. package/test/normalizer.test.ts +66 -0
  66. package/test/scheduler.test.ts +674 -0
  67. package/test/severity.test.ts +122 -0
  68. package/tsconfig.json +10 -0
  69. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * URL Content Checker
3
+ * Monitors documentation URLs for content changes using SHA256 hashing
4
+ */
5
+
6
+ import type { Checker, DependencySnapshot, ChangeDetection, AccessConfig } from '../types.js';
7
+ import { normalizeHTML } from '../normalizer.js';
8
+ import crypto from 'node:crypto';
9
+
10
+ export class URLContentChecker implements Checker {
11
+ /**
12
+ * Fetches and hashes URL content
13
+ */
14
+ async fetch(config: AccessConfig): Promise<DependencySnapshot> {
15
+ const { url } = config;
16
+
17
+ try {
18
+ const response = await fetch(url, {
19
+ headers: {
20
+ 'User-Agent': 'dependabit-monitor/1.0'
21
+ }
22
+ });
23
+
24
+ if (!response.ok) {
25
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
26
+ }
27
+
28
+ const contentType = response.headers.get('content-type') || '';
29
+ const content = await response.text();
30
+
31
+ let normalizedContent: string;
32
+
33
+ // Apply HTML normalization if content is HTML
34
+ if (
35
+ contentType.includes('text/html') ||
36
+ content.trim().startsWith('<!DOCTYPE') ||
37
+ content.trim().startsWith('<html')
38
+ ) {
39
+ normalizedContent = normalizeHTML(content);
40
+ } else {
41
+ // For non-HTML content (markdown, plain text, etc.), just normalize whitespace
42
+ normalizedContent = content.replace(/\s+/g, ' ').trim();
43
+ }
44
+
45
+ // Generate SHA256 hash of normalized content
46
+ const stateHash = crypto.createHash('sha256').update(normalizedContent).digest('hex');
47
+
48
+ return {
49
+ stateHash,
50
+ fetchedAt: new Date(),
51
+ metadata: {
52
+ contentType,
53
+ contentLength: content.length,
54
+ normalizedLength: normalizedContent.length
55
+ }
56
+ };
57
+ } catch (error) {
58
+ throw new Error(`Failed to fetch URL content: ${(error as Error).message}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Compares two snapshots to detect content changes
64
+ */
65
+ async compare(prev: DependencySnapshot, curr: DependencySnapshot): Promise<ChangeDetection> {
66
+ const changes: string[] = [];
67
+
68
+ // Content changed if hashes differ
69
+ if (prev.stateHash !== curr.stateHash) {
70
+ changes.push('content');
71
+ }
72
+
73
+ return {
74
+ hasChanged: changes.length > 0,
75
+ changes
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * State Comparator
3
+ * Generic comparison logic for dependency state snapshots
4
+ */
5
+
6
+ import type { DependencySnapshot, ChangeDetection } from './types.js';
7
+
8
+ export class StateComparator {
9
+ /**
10
+ * Compares two dependency snapshots to detect changes
11
+ */
12
+ compare(oldState: DependencySnapshot, newState: DependencySnapshot): ChangeDetection {
13
+ const changes: string[] = [];
14
+
15
+ // Check state hash
16
+ if (oldState.stateHash !== newState.stateHash) {
17
+ changes.push('stateHash');
18
+ }
19
+
20
+ // Check version
21
+ if (oldState.version !== newState.version) {
22
+ changes.push('version');
23
+ }
24
+
25
+ // Check metadata changes (shallow comparison)
26
+ if (this.hasMetadataChanges(oldState.metadata, newState.metadata)) {
27
+ changes.push('metadata');
28
+ }
29
+
30
+ return {
31
+ hasChanged: changes.length > 0,
32
+ changes,
33
+ oldVersion: oldState.version,
34
+ newVersion: newState.version
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Checks if metadata has changed (shallow comparison)
40
+ */
41
+ private hasMetadataChanges(
42
+ oldMeta?: Record<string, unknown>,
43
+ newMeta?: Record<string, unknown>
44
+ ): boolean {
45
+ if (!oldMeta && !newMeta) {
46
+ return false;
47
+ }
48
+
49
+ if (!oldMeta || !newMeta) {
50
+ return true;
51
+ }
52
+
53
+ const oldKeys = Object.keys(oldMeta);
54
+ const newKeys = Object.keys(newMeta);
55
+
56
+ if (oldKeys.length !== newKeys.length) {
57
+ return true;
58
+ }
59
+
60
+ for (const key of oldKeys) {
61
+ if (oldMeta[key] !== newMeta[key]) {
62
+ return true;
63
+ }
64
+ }
65
+
66
+ return false;
67
+ }
68
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @dependabit/monitor - Dependency change detection and monitoring
3
+ */
4
+
5
+ export { Monitor } from './monitor.js';
6
+ export type { DependencyConfig, CheckResult } from './monitor.js';
7
+
8
+ export { GitHubRepoChecker } from './checkers/github-repo.js';
9
+ export { URLContentChecker } from './checkers/url-content.js';
10
+ export { OpenAPIChecker } from './checkers/openapi.js';
11
+
12
+ export { StateComparator } from './comparator.js';
13
+ export { SeverityClassifier } from './severity.js';
14
+ export type { Severity } from './severity.js';
15
+
16
+ export { normalizeHTML, normalizeURL } from './normalizer.js';
17
+
18
+ export { Scheduler } from './scheduler.js';
19
+
20
+ export type { Checker, DependencySnapshot, ChangeDetection, AccessConfig } from './types.js';
package/src/monitor.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Monitor Orchestrator
3
+ * Coordinates dependency checking across multiple access methods
4
+ */
5
+
6
+ import { GitHubRepoChecker } from './checkers/github-repo.js';
7
+ import { URLContentChecker } from './checkers/url-content.js';
8
+ import { OpenAPIChecker } from './checkers/openapi.js';
9
+ import { SeverityClassifier } from './severity.js';
10
+ import type { Checker, AccessConfig, DependencySnapshot, ChangeDetection } from './types.js';
11
+
12
+ export interface DependencyConfig extends AccessConfig {
13
+ id: string;
14
+ name?: string;
15
+ type?: string;
16
+ currentStateHash: string;
17
+ currentVersion?: string;
18
+ lastChecked?: string;
19
+ monitoring?: {
20
+ enabled?: boolean;
21
+ ignoreChanges?: boolean;
22
+ };
23
+ }
24
+
25
+ export interface CheckResult {
26
+ dependency: DependencyConfig;
27
+ hasChanged: boolean;
28
+ changes?: ChangeDetection;
29
+ severity?: 'breaking' | 'major' | 'minor' | undefined;
30
+ newSnapshot?: DependencySnapshot;
31
+ error?: string;
32
+ }
33
+
34
+ export class Monitor {
35
+ private checkers: Map<string, Checker>;
36
+ private classifier: SeverityClassifier;
37
+
38
+ constructor() {
39
+ this.checkers = new Map();
40
+ this.checkers.set('github-api', new GitHubRepoChecker());
41
+ this.checkers.set('http', new URLContentChecker());
42
+ this.checkers.set('openapi', new OpenAPIChecker());
43
+
44
+ this.classifier = new SeverityClassifier();
45
+ }
46
+
47
+ /**
48
+ * Checks a single dependency for changes
49
+ */
50
+ async checkDependency(dependency: DependencyConfig): Promise<CheckResult> {
51
+ try {
52
+ // Get appropriate checker for access method
53
+ const checker = this.checkers.get(dependency.accessMethod);
54
+ if (!checker) {
55
+ throw new Error(`Unsupported access method: ${dependency.accessMethod}`);
56
+ }
57
+
58
+ // Fetch current state
59
+ const newSnapshot = await checker.fetch(dependency);
60
+
61
+ // Create old snapshot from stored data
62
+ const oldSnapshot: DependencySnapshot = {
63
+ stateHash: dependency.currentStateHash,
64
+ version: dependency.currentVersion,
65
+ fetchedAt: dependency.lastChecked ? new Date(dependency.lastChecked) : new Date()
66
+ };
67
+
68
+ // Compare states
69
+ const changes = await checker.compare(oldSnapshot, newSnapshot);
70
+
71
+ // Classify severity if changes detected
72
+ let severity: 'breaking' | 'major' | 'minor' | undefined;
73
+ if (changes.hasChanged) {
74
+ severity = this.classifier.classify(changes);
75
+ }
76
+
77
+ return {
78
+ dependency,
79
+ hasChanged: changes.hasChanged,
80
+ changes,
81
+ severity,
82
+ newSnapshot
83
+ };
84
+ } catch (error) {
85
+ return {
86
+ dependency,
87
+ hasChanged: false,
88
+ error: (error as Error).message
89
+ };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Checks multiple dependencies, respecting monitoring rules
95
+ */
96
+ async checkAll(dependencies: DependencyConfig[]): Promise<CheckResult[]> {
97
+ // Filter out disabled dependencies
98
+ const enabledDeps = dependencies.filter((dep) => {
99
+ if (dep.monitoring?.enabled === false) {
100
+ return false;
101
+ }
102
+ if (dep.monitoring?.ignoreChanges === true) {
103
+ return false;
104
+ }
105
+ return true;
106
+ });
107
+
108
+ // Check all dependencies in parallel
109
+ const results = await Promise.all(enabledDeps.map((dep) => this.checkDependency(dep)));
110
+
111
+ return results;
112
+ }
113
+
114
+ /**
115
+ * Registers a custom checker for an access method
116
+ */
117
+ registerChecker(accessMethod: string, checker: Checker): void {
118
+ this.checkers.set(accessMethod, checker);
119
+ }
120
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * HTML Normalizer for content-based change detection
3
+ *
4
+ * Implements 6-step normalization to reduce false positives:
5
+ * 1. Remove <script> and <style> tags
6
+ * 2. Strip HTML comments
7
+ * 3. Normalize whitespace
8
+ * 4. Remove timestamp patterns
9
+ * 5. Remove analytics/tracking parameters
10
+ * 6. Preserve meaningful content
11
+ */
12
+
13
+ /**
14
+ * Normalizes HTML content for consistent comparison
15
+ * @param html Raw HTML content
16
+ * @returns Normalized HTML string
17
+ */
18
+ export function normalizeHTML(html: string): string {
19
+ if (!html || html.trim() === '') {
20
+ return '';
21
+ }
22
+
23
+ let normalized = html;
24
+
25
+ // Step 1: Remove script and style tags (with content)
26
+ // Use a simpler, more secure approach - remove everything between opening and closing tags
27
+ // This handles variations like </script >, </script\n>, etc.
28
+ let prevLength = 0;
29
+ while (normalized.length !== prevLength) {
30
+ prevLength = normalized.length;
31
+ // Match <script...> then any content, then </script with any whitespace/attributes before >
32
+ normalized = normalized.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*[^>]*>/gi, '');
33
+ }
34
+ prevLength = 0;
35
+ while (normalized.length !== prevLength) {
36
+ prevLength = normalized.length;
37
+ normalized = normalized.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*[^>]*>/gi, '');
38
+ }
39
+
40
+ // Step 2: Strip HTML comments - iterative to handle nested comments
41
+ prevLength = 0;
42
+ while (normalized.length !== prevLength) {
43
+ prevLength = normalized.length;
44
+ normalized = normalized.replace(/<!--[\s\S]*?-->/g, '');
45
+ }
46
+
47
+ // Step 3: Normalize whitespace (collapse multiple spaces/newlines)
48
+ normalized = normalized.replace(/\s+/g, ' ');
49
+ normalized = normalized.trim();
50
+
51
+ // Step 4: Remove common timestamp patterns
52
+ // Patterns like "Updated: 2024-01-01" or "Last modified: Jan 1, 2024"
53
+ normalized = normalized.replace(
54
+ /\b(Updated|Last\s+modified|Modified|Created|Published):\s*[^<\n]+/gi,
55
+ ''
56
+ );
57
+
58
+ // Remove ISO timestamps
59
+ normalized = normalized.replace(
60
+ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?/g,
61
+ ''
62
+ );
63
+
64
+ // Remove common date formats
65
+ normalized = normalized.replace(
66
+ /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},?\s+\d{4}\b/gi,
67
+ ''
68
+ );
69
+
70
+ // Step 5: Remove analytics/tracking parameters from URLs
71
+ // UTM parameters, tracking IDs, session IDs, etc.
72
+ normalized = normalized.replace(
73
+ /[?&](utm_[^&\s"']+|fbclid|gclid|msclkid|mc_[^&\s"']+|_ga|sessionid)[^&\s"']*/gi,
74
+ ''
75
+ );
76
+
77
+ // Step 6: Final cleanup - preserve meaningful content structure
78
+ // Remove extra spaces that may have been introduced
79
+ normalized = normalized.replace(/\s+/g, ' ');
80
+ normalized = normalized.trim();
81
+
82
+ return normalized;
83
+ }
84
+
85
+ /**
86
+ * Normalizes a URL by removing tracking parameters
87
+ * @param url URL string
88
+ * @returns Normalized URL
89
+ */
90
+ export function normalizeURL(url: string): string {
91
+ try {
92
+ const urlObj = new URL(url);
93
+
94
+ // Remove common tracking parameters
95
+ const trackingParams = [
96
+ 'utm_source',
97
+ 'utm_medium',
98
+ 'utm_campaign',
99
+ 'utm_term',
100
+ 'utm_content',
101
+ 'fbclid',
102
+ 'gclid',
103
+ 'msclkid',
104
+ 'mc_cid',
105
+ 'mc_eid',
106
+ '_ga',
107
+ '_gac',
108
+ 'sessionid',
109
+ 'sid',
110
+ 'SSID'
111
+ ];
112
+
113
+ trackingParams.forEach((param) => {
114
+ urlObj.searchParams.delete(param);
115
+ });
116
+
117
+ return urlObj.toString();
118
+ } catch {
119
+ // If URL parsing fails, return original
120
+ return url;
121
+ }
122
+ }
@@ -0,0 +1,175 @@
1
+ import {
2
+ type DependencyEntry,
3
+ type DependabitConfig,
4
+ getEffectiveMonitoringRules
5
+ } from '@dependabit/manifest';
6
+
7
+ export interface SchedulerOptions {
8
+ currentTime?: Date;
9
+ }
10
+
11
+ /**
12
+ * Scheduler for per-dependency monitoring
13
+ *
14
+ * Determines which dependencies should be checked based on:
15
+ * - Check frequency (hourly, daily, weekly, monthly)
16
+ * - Last checked timestamp
17
+ * - Enabled/disabled status
18
+ * - IgnoreChanges flag
19
+ * - Config overrides
20
+ */
21
+ export class Scheduler {
22
+ /**
23
+ * Check if a dependency should be checked now
24
+ *
25
+ * @param dependency Dependency entry
26
+ * @param config Configuration
27
+ * @param currentTime Current time (defaults to now)
28
+ * @returns true if dependency should be checked
29
+ */
30
+ shouldCheckDependency(
31
+ dependency: DependencyEntry,
32
+ config: DependabitConfig,
33
+ currentTime: Date = new Date()
34
+ ): boolean {
35
+ // Check dependency's own monitoring rules first
36
+ if (dependency.monitoring) {
37
+ if (!dependency.monitoring.enabled) {
38
+ return false;
39
+ }
40
+
41
+ if (dependency.monitoring.ignoreChanges) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ // Get effective monitoring rules (applies config overrides)
47
+ const rules = getEffectiveMonitoringRules(config, dependency.url);
48
+
49
+ // Check if monitoring is enabled at config level
50
+ if (!rules.enabled) {
51
+ return false;
52
+ }
53
+
54
+ // Check if changes should be ignored at config level
55
+ if (rules.ignoreChanges) {
56
+ return false;
57
+ }
58
+
59
+ // Get last checked time
60
+ const lastChecked = new Date(dependency.lastChecked);
61
+ const timeSinceCheck = currentTime.getTime() - lastChecked.getTime();
62
+
63
+ // Determine frequency to use (dependency's own monitoring rules take precedence)
64
+ const checkFrequency = dependency.monitoring?.checkFrequency || rules.checkFrequency;
65
+
66
+ // Determine if enough time has passed based on frequency
67
+ const intervalMs = this.getIntervalMs(checkFrequency);
68
+
69
+ return timeSinceCheck >= intervalMs;
70
+ }
71
+
72
+ /**
73
+ * Filter dependencies that should be checked
74
+ *
75
+ * @param dependencies Array of dependencies
76
+ * @param config Configuration
77
+ * @param currentTime Current time (defaults to now)
78
+ * @returns Filtered array of dependencies to check
79
+ */
80
+ filterDependenciesToCheck(
81
+ dependencies: DependencyEntry[],
82
+ config: DependabitConfig,
83
+ currentTime: Date = new Date()
84
+ ): DependencyEntry[] {
85
+ return dependencies.filter((dep) => this.shouldCheckDependency(dep, config, currentTime));
86
+ }
87
+
88
+ /**
89
+ * Get interval in milliseconds for a check frequency
90
+ *
91
+ * @param frequency Check frequency
92
+ * @returns Interval in milliseconds
93
+ */
94
+ private getIntervalMs(frequency: 'hourly' | 'daily' | 'weekly' | 'monthly'): number {
95
+ switch (frequency) {
96
+ case 'hourly':
97
+ return 60 * 60 * 1000; // 1 hour
98
+ case 'daily':
99
+ return 24 * 60 * 60 * 1000; // 24 hours
100
+ case 'weekly':
101
+ return 7 * 24 * 60 * 60 * 1000; // 7 days
102
+ case 'monthly':
103
+ return 30 * 24 * 60 * 60 * 1000; // 30 days (approximate)
104
+ default:
105
+ return 24 * 60 * 60 * 1000; // Default to daily
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get next check time for a dependency
111
+ *
112
+ * @param dependency Dependency entry
113
+ * @param config Configuration
114
+ * @returns Next check time
115
+ */
116
+ getNextCheckTime(dependency: DependencyEntry, config: DependabitConfig): Date {
117
+ const rules = getEffectiveMonitoringRules(config, dependency.url);
118
+ const lastChecked = new Date(dependency.lastChecked);
119
+
120
+ // Determine frequency to use (dependency's own monitoring rules take precedence)
121
+ const checkFrequency = dependency.monitoring?.checkFrequency || rules.checkFrequency;
122
+ const intervalMs = this.getIntervalMs(checkFrequency);
123
+
124
+ return new Date(lastChecked.getTime() + intervalMs);
125
+ }
126
+
127
+ /**
128
+ * Get schedule summary for all dependencies
129
+ *
130
+ * @param dependencies Array of dependencies
131
+ * @param config Configuration
132
+ * @returns Schedule summary grouped by frequency
133
+ */
134
+ getScheduleSummary(
135
+ dependencies: DependencyEntry[],
136
+ config: DependabitConfig
137
+ ): {
138
+ hourly: number;
139
+ daily: number;
140
+ weekly: number;
141
+ monthly: number;
142
+ disabled: number;
143
+ } {
144
+ const summary = {
145
+ hourly: 0,
146
+ daily: 0,
147
+ weekly: 0,
148
+ monthly: 0,
149
+ disabled: 0
150
+ };
151
+
152
+ for (const dep of dependencies) {
153
+ // Check dependency's own monitoring rules first
154
+ if (dep.monitoring) {
155
+ if (!dep.monitoring.enabled || dep.monitoring.ignoreChanges) {
156
+ summary.disabled++;
157
+ continue;
158
+ }
159
+ }
160
+
161
+ const rules = getEffectiveMonitoringRules(config, dep.url);
162
+
163
+ if (!rules.enabled || rules.ignoreChanges) {
164
+ summary.disabled++;
165
+ } else {
166
+ // Use dependency-level frequency if available, otherwise use rules
167
+ const checkFrequency: 'hourly' | 'daily' | 'weekly' | 'monthly' =
168
+ dep.monitoring?.checkFrequency ?? rules.checkFrequency;
169
+ summary[checkFrequency]++;
170
+ }
171
+ }
172
+
173
+ return summary;
174
+ }
175
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Severity Classifier
3
+ * Classifies dependency changes into breaking, major, or minor severity levels
4
+ */
5
+
6
+ import type { ChangeDetection } from './types.js';
7
+
8
+ export type Severity = 'breaking' | 'major' | 'minor';
9
+
10
+ export class SeverityClassifier {
11
+ /**
12
+ * Classifies the severity of a change based on version changes and change types
13
+ */
14
+ classify(changes: ChangeDetection): Severity {
15
+ if (!changes.hasChanged) {
16
+ return 'minor';
17
+ }
18
+
19
+ // Check for version-based classification
20
+ if (changes.oldVersion && changes.newVersion) {
21
+ const severity = this.classifyVersionChange(changes.oldVersion, changes.newVersion);
22
+ if (severity) {
23
+ return severity;
24
+ }
25
+ }
26
+
27
+ // Check for OpenAPI/schema changes
28
+ if (changes.diff && typeof changes.diff === 'object') {
29
+ const diff = changes.diff as {
30
+ removedEndpoints?: string[];
31
+ schemaChanges?: string[];
32
+ addedEndpoints?: string[];
33
+ };
34
+
35
+ // Breaking: Removed endpoints or incompatible schema changes
36
+ if (diff.removedEndpoints && diff.removedEndpoints.length > 0) {
37
+ return 'breaking';
38
+ }
39
+
40
+ if (diff.schemaChanges && diff.schemaChanges.length > 0) {
41
+ return 'breaking';
42
+ }
43
+
44
+ // Major: New endpoints or features
45
+ if (diff.addedEndpoints && diff.addedEndpoints.length > 0) {
46
+ return 'major';
47
+ }
48
+ }
49
+
50
+ // Default to minor for content changes
51
+ return 'minor';
52
+ }
53
+
54
+ /**
55
+ * Classifies severity based on semantic versioning
56
+ * @returns Severity level or undefined if version format not recognized
57
+ */
58
+ private classifyVersionChange(oldVersion: string, newVersion: string): Severity | undefined {
59
+ // Parse semver-like versions
60
+ const oldParts = this.parseVersion(oldVersion);
61
+ const newParts = this.parseVersion(newVersion);
62
+
63
+ if (!oldParts || !newParts) {
64
+ return undefined;
65
+ }
66
+
67
+ const [oldMajor, oldMinor, oldPatch] = oldParts;
68
+ const [newMajor, newMinor, newPatch] = newParts;
69
+
70
+ // Breaking: Major version increase
71
+ if (newMajor > oldMajor) {
72
+ return 'breaking';
73
+ }
74
+
75
+ // Major: Minor version increase
76
+ if (newMajor === oldMajor && newMinor > oldMinor) {
77
+ return 'major';
78
+ }
79
+
80
+ // Minor: Patch version increase or same version
81
+ if (newMajor === oldMajor && newMinor === oldMinor && newPatch >= oldPatch) {
82
+ return 'minor';
83
+ }
84
+
85
+ // Default for other cases
86
+ return 'major';
87
+ }
88
+
89
+ /**
90
+ * Parses a version string into [major, minor, patch]
91
+ */
92
+ private parseVersion(version: string): [number, number, number] | null {
93
+ if (!version) {
94
+ return null;
95
+ }
96
+
97
+ // Remove 'v' prefix if present
98
+ const cleaned = version.replace(/^v/, '');
99
+
100
+ // Match semver pattern
101
+ const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
102
+ if (!match) {
103
+ return null;
104
+ }
105
+
106
+ return [
107
+ parseInt(match[1] || '0', 10),
108
+ parseInt(match[2] || '0', 10),
109
+ parseInt(match[3] || '0', 10)
110
+ ];
111
+ }
112
+ }