@rigour-labs/core 2.9.4 → 2.10.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 (38) hide show
  1. package/dist/index.js +4 -0
  2. package/dist/pattern-index/embeddings.d.ts +19 -0
  3. package/dist/pattern-index/embeddings.js +78 -0
  4. package/dist/pattern-index/index.d.ts +11 -0
  5. package/dist/pattern-index/index.js +15 -0
  6. package/dist/pattern-index/indexer.d.ts +101 -0
  7. package/dist/pattern-index/indexer.js +573 -0
  8. package/dist/pattern-index/indexer.test.d.ts +6 -0
  9. package/dist/pattern-index/indexer.test.js +188 -0
  10. package/dist/pattern-index/matcher.d.ts +106 -0
  11. package/dist/pattern-index/matcher.js +376 -0
  12. package/dist/pattern-index/matcher.test.d.ts +6 -0
  13. package/dist/pattern-index/matcher.test.js +238 -0
  14. package/dist/pattern-index/overrides.d.ts +64 -0
  15. package/dist/pattern-index/overrides.js +196 -0
  16. package/dist/pattern-index/security.d.ts +25 -0
  17. package/dist/pattern-index/security.js +127 -0
  18. package/dist/pattern-index/staleness.d.ts +71 -0
  19. package/dist/pattern-index/staleness.js +381 -0
  20. package/dist/pattern-index/staleness.test.d.ts +6 -0
  21. package/dist/pattern-index/staleness.test.js +211 -0
  22. package/dist/pattern-index/types.d.ts +221 -0
  23. package/dist/pattern-index/types.js +7 -0
  24. package/package.json +14 -1
  25. package/src/index.ts +4 -0
  26. package/src/pattern-index/embeddings.ts +84 -0
  27. package/src/pattern-index/index.ts +53 -0
  28. package/src/pattern-index/indexer.test.ts +276 -0
  29. package/src/pattern-index/indexer.ts +693 -0
  30. package/src/pattern-index/matcher.test.ts +293 -0
  31. package/src/pattern-index/matcher.ts +493 -0
  32. package/src/pattern-index/overrides.ts +235 -0
  33. package/src/pattern-index/security.ts +151 -0
  34. package/src/pattern-index/staleness.test.ts +313 -0
  35. package/src/pattern-index/staleness.ts +438 -0
  36. package/src/pattern-index/types.ts +339 -0
  37. package/vitest.config.ts +7 -0
  38. package/vitest.setup.ts +30 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Pattern Matcher Tests
3
+ *
4
+ * Comprehensive tests for the pattern matcher.
5
+ */
6
+ import { describe, it, expect } from 'vitest';
7
+ import { PatternMatcher, checkPatternDuplicate } from './matcher.js';
8
+ // Helper to create a mock pattern
9
+ function createPattern(overrides = {}) {
10
+ return {
11
+ id: 'test-id',
12
+ type: 'function',
13
+ name: 'testFunction',
14
+ file: 'src/utils.ts',
15
+ line: 1,
16
+ endLine: 5,
17
+ signature: '(input: string): string',
18
+ description: 'A test function',
19
+ keywords: ['test', 'function'],
20
+ hash: 'abc123',
21
+ exported: true,
22
+ usageCount: 0,
23
+ indexedAt: new Date().toISOString(),
24
+ ...overrides
25
+ };
26
+ }
27
+ // Helper to create a mock index
28
+ function createIndex(patterns) {
29
+ return {
30
+ version: '1.0.0',
31
+ lastUpdated: new Date().toISOString(),
32
+ rootDir: '/test',
33
+ patterns,
34
+ stats: {
35
+ totalPatterns: patterns.length,
36
+ totalFiles: 1,
37
+ byType: { function: patterns.length },
38
+ indexDurationMs: 100
39
+ },
40
+ files: []
41
+ };
42
+ }
43
+ describe('PatternMatcher', () => {
44
+ describe('exact name match', () => {
45
+ it('should find exact name matches with 100% confidence', async () => {
46
+ const pattern = createPattern({ name: 'formatDate' });
47
+ const index = createIndex([pattern]);
48
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
49
+ const result = await matcher.match({ name: 'formatDate' });
50
+ expect(result.status).toBe('FOUND_SIMILAR');
51
+ expect(result.matches).toHaveLength(1);
52
+ expect(result.matches[0].matchType).toBe('exact');
53
+ expect(result.matches[0].confidence).toBe(100);
54
+ });
55
+ it('should be case-insensitive for exact matches', async () => {
56
+ const pattern = createPattern({ name: 'formatDate' });
57
+ const index = createIndex([pattern]);
58
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
59
+ const result = await matcher.match({ name: 'FormatDate' });
60
+ expect(result.matches).toHaveLength(1);
61
+ expect(result.matches[0].matchType).toBe('exact');
62
+ });
63
+ it('should set action to BLOCK for exact matches', async () => {
64
+ const pattern = createPattern({ name: 'formatDate' });
65
+ const index = createIndex([pattern]);
66
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
67
+ const result = await matcher.match({ name: 'formatDate' });
68
+ expect(result.action).toBe('BLOCK');
69
+ });
70
+ });
71
+ describe('fuzzy name match', () => {
72
+ it('should find fuzzy matches for similar names', async () => {
73
+ const pattern = createPattern({ name: 'formatDate' });
74
+ const index = createIndex([pattern]);
75
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
76
+ // 'formatDate' vs 'formatDates' have extremely high similarity
77
+ const result = await matcher.match({ name: 'formatDates' });
78
+ expect(result.status).toBe('FOUND_SIMILAR');
79
+ expect(result.matches.some(m => m.matchType === 'fuzzy')).toBe(true);
80
+ });
81
+ it('should find matches for renamed patterns', async () => {
82
+ const pattern = createPattern({
83
+ name: 'formatDate',
84
+ keywords: ['format', 'date']
85
+ });
86
+ const index = createIndex([pattern]);
87
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
88
+ const result = await matcher.match({ name: 'formatDateString' });
89
+ expect(result.matches.length).toBeGreaterThan(0);
90
+ });
91
+ it('should not match completely unrelated names', async () => {
92
+ const pattern = createPattern({ name: 'formatDate' });
93
+ const index = createIndex([pattern]);
94
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
95
+ const result = await matcher.match({ name: 'sendEmail' });
96
+ expect(result.status).toBe('NO_MATCH');
97
+ });
98
+ });
99
+ describe('signature match', () => {
100
+ it('should find matches with identical signatures', async () => {
101
+ const pattern = createPattern({
102
+ name: 'formatDate',
103
+ signature: '(date: Date): string'
104
+ });
105
+ const index = createIndex([pattern]);
106
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
107
+ const result = await matcher.match({
108
+ name: 'myDateFormatter',
109
+ signature: '(date: Date): string'
110
+ });
111
+ expect(result.matches.some(m => m.matchType === 'signature')).toBe(true);
112
+ });
113
+ it('should match similar parameter patterns', async () => {
114
+ const pattern = createPattern({
115
+ name: 'formatDate',
116
+ signature: '(date: Date, format: string): string'
117
+ });
118
+ const index = createIndex([pattern]);
119
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
120
+ const result = await matcher.match({
121
+ signature: '(input: Date, pattern: string): string'
122
+ });
123
+ expect(result.matches.length).toBeGreaterThan(0);
124
+ });
125
+ });
126
+ describe('keyword match', () => {
127
+ it('should find matches based on keywords', async () => {
128
+ const pattern = createPattern({
129
+ name: 'formatDate',
130
+ keywords: ['format', 'date', 'time']
131
+ });
132
+ const index = createIndex([pattern]);
133
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
134
+ const result = await matcher.match({
135
+ keywords: ['date', 'format']
136
+ });
137
+ expect(result.matches.some(m => m.matchType === 'semantic')).toBe(true);
138
+ });
139
+ });
140
+ describe('overrides', () => {
141
+ it('should allow overridden patterns', async () => {
142
+ const pattern = createPattern({ name: 'formatDate' });
143
+ const index = createIndex([pattern]);
144
+ const matcher = new PatternMatcher(index, { useEmbeddings: false }, [
145
+ {
146
+ pattern: 'formatDate',
147
+ reason: 'Refactoring in progress',
148
+ createdAt: new Date().toISOString()
149
+ }
150
+ ]);
151
+ const result = await matcher.match({ name: 'formatDate' });
152
+ expect(result.status).toBe('OVERRIDE_ALLOWED');
153
+ expect(result.action).toBe('ALLOW');
154
+ });
155
+ it('should support glob overrides', async () => {
156
+ const pattern = createPattern({ name: 'formatDate' });
157
+ const index = createIndex([pattern]);
158
+ const matcher = new PatternMatcher(index, { useEmbeddings: false }, [
159
+ {
160
+ pattern: 'format*',
161
+ reason: 'All format functions are exempt',
162
+ createdAt: new Date().toISOString()
163
+ }
164
+ ]);
165
+ const result = await matcher.match({ name: 'formatDate' });
166
+ expect(result.status).toBe('OVERRIDE_ALLOWED');
167
+ });
168
+ it('should ignore expired overrides', async () => {
169
+ const pattern = createPattern({ name: 'formatDate' });
170
+ const index = createIndex([pattern]);
171
+ const matcher = new PatternMatcher(index, { useEmbeddings: false }, [
172
+ {
173
+ pattern: 'formatDate',
174
+ reason: 'Expired override',
175
+ createdAt: '2020-01-01T00:00:00Z',
176
+ expiresAt: '2020-01-02T00:00:00Z' // Expired
177
+ }
178
+ ]);
179
+ const result = await matcher.match({ name: 'formatDate' });
180
+ expect(result.status).toBe('FOUND_SIMILAR');
181
+ });
182
+ });
183
+ describe('configuration', () => {
184
+ it('should respect minConfidence setting', async () => {
185
+ const pattern = createPattern({ name: 'formatDate' });
186
+ const index = createIndex([pattern]);
187
+ const matcher = new PatternMatcher(index, { minConfidence: 95, useEmbeddings: false });
188
+ // Fuzzy match won't meet 95% threshold
189
+ const result = await matcher.match({ name: 'dateFormat' });
190
+ // Should only include exact matches at 95%+ threshold
191
+ expect(result.matches.every(m => m.confidence >= 95)).toBe(true);
192
+ });
193
+ it('should respect maxMatches setting', async () => {
194
+ const patterns = [
195
+ createPattern({ id: '1', name: 'formatDate1' }),
196
+ createPattern({ id: '2', name: 'formatDate2' }),
197
+ createPattern({ id: '3', name: 'formatDate3' }),
198
+ createPattern({ id: '4', name: 'formatDate4' }),
199
+ ];
200
+ const index = createIndex(patterns);
201
+ const matcher = new PatternMatcher(index, { maxMatches: 2, useEmbeddings: false });
202
+ const result = await matcher.match({ name: 'formatDate' });
203
+ expect(result.matches.length).toBeLessThanOrEqual(2);
204
+ });
205
+ });
206
+ describe('suggestions', () => {
207
+ it('should suggest importing exported patterns', async () => {
208
+ const pattern = createPattern({
209
+ name: 'formatDate',
210
+ file: 'src/utils/date.ts',
211
+ exported: true
212
+ });
213
+ const index = createIndex([pattern]);
214
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
215
+ const result = await matcher.match({ name: 'formatDate' });
216
+ expect(result.suggestion).toContain('Import');
217
+ expect(result.suggestion).toContain('src/utils/date.ts');
218
+ });
219
+ it('should suggest extracting non-exported patterns', async () => {
220
+ const pattern = createPattern({
221
+ name: 'formatDate',
222
+ exported: false
223
+ });
224
+ const index = createIndex([pattern]);
225
+ const matcher = new PatternMatcher(index, { useEmbeddings: false });
226
+ const result = await matcher.match({ name: 'formatDate' });
227
+ expect(result.suggestion).toContain('similar pattern');
228
+ });
229
+ });
230
+ });
231
+ describe('checkPatternDuplicate', () => {
232
+ it('should be a quick helper for duplicate checking', async () => {
233
+ const pattern = createPattern({ name: 'myUtil' });
234
+ const index = createIndex([pattern]);
235
+ const result = await checkPatternDuplicate(index, 'myUtil');
236
+ expect(result.status).toBe('FOUND_SIMILAR');
237
+ });
238
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Human Override Manager
3
+ *
4
+ * Manages human overrides for pattern matching.
5
+ * Supports inline comments, config files, and MCP approvals.
6
+ */
7
+ import type { PatternOverride } from './types.js';
8
+ /**
9
+ * Override Manager class.
10
+ */
11
+ export declare class OverrideManager {
12
+ private overrides;
13
+ private rootDir;
14
+ private overridesPath;
15
+ constructor(rootDir: string);
16
+ /**
17
+ * Load overrides from disk.
18
+ */
19
+ load(): Promise<PatternOverride[]>;
20
+ /**
21
+ * Save overrides to disk.
22
+ */
23
+ save(): Promise<void>;
24
+ /**
25
+ * Add a new override.
26
+ */
27
+ addOverride(override: Omit<PatternOverride, 'createdAt'>): Promise<PatternOverride>;
28
+ /**
29
+ * Remove an override.
30
+ */
31
+ removeOverride(pattern: string): Promise<boolean>;
32
+ /**
33
+ * Check if a pattern is overridden.
34
+ */
35
+ isOverridden(name: string): PatternOverride | null;
36
+ /**
37
+ * Get all active overrides.
38
+ */
39
+ getActiveOverrides(): PatternOverride[];
40
+ /**
41
+ * Get all expired overrides.
42
+ */
43
+ getExpiredOverrides(): PatternOverride[];
44
+ /**
45
+ * Clean up expired overrides.
46
+ */
47
+ cleanupExpired(): Promise<number>;
48
+ /**
49
+ * Parse inline override comments from code.
50
+ */
51
+ parseInlineOverrides(code: string, filePath: string): PatternOverride[];
52
+ /**
53
+ * Check if an override is expired.
54
+ */
55
+ private isExpired;
56
+ /**
57
+ * Get default expiration date.
58
+ */
59
+ private getDefaultExpiration;
60
+ }
61
+ /**
62
+ * Load overrides from rigour.config.yaml.
63
+ */
64
+ export declare function loadConfigOverrides(rootDir: string): Promise<PatternOverride[]>;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Human Override Manager
3
+ *
4
+ * Manages human overrides for pattern matching.
5
+ * Supports inline comments, config files, and MCP approvals.
6
+ */
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ /** Default override expiration in days */
10
+ const DEFAULT_EXPIRATION_DAYS = 30;
11
+ /**
12
+ * Override Manager class.
13
+ */
14
+ export class OverrideManager {
15
+ overrides = [];
16
+ rootDir;
17
+ overridesPath;
18
+ constructor(rootDir) {
19
+ this.rootDir = rootDir;
20
+ this.overridesPath = path.join(rootDir, '.rigour', 'allow.json');
21
+ }
22
+ /**
23
+ * Load overrides from disk.
24
+ */
25
+ async load() {
26
+ try {
27
+ const content = await fs.readFile(this.overridesPath, 'utf-8');
28
+ const data = JSON.parse(content);
29
+ this.overrides = data.overrides || [];
30
+ // Filter out expired overrides
31
+ this.overrides = this.overrides.filter(o => !this.isExpired(o));
32
+ return this.overrides;
33
+ }
34
+ catch {
35
+ this.overrides = [];
36
+ return [];
37
+ }
38
+ }
39
+ /**
40
+ * Save overrides to disk.
41
+ */
42
+ async save() {
43
+ const dir = path.dirname(this.overridesPath);
44
+ await fs.mkdir(dir, { recursive: true });
45
+ await fs.writeFile(this.overridesPath, JSON.stringify({ overrides: this.overrides }, null, 2), 'utf-8');
46
+ }
47
+ /**
48
+ * Add a new override.
49
+ */
50
+ async addOverride(override) {
51
+ const fullOverride = {
52
+ ...override,
53
+ createdAt: new Date().toISOString(),
54
+ expiresAt: override.expiresAt || this.getDefaultExpiration()
55
+ };
56
+ // Remove any existing override for the same pattern
57
+ this.overrides = this.overrides.filter(o => o.pattern !== fullOverride.pattern);
58
+ this.overrides.push(fullOverride);
59
+ await this.save();
60
+ return fullOverride;
61
+ }
62
+ /**
63
+ * Remove an override.
64
+ */
65
+ async removeOverride(pattern) {
66
+ const before = this.overrides.length;
67
+ this.overrides = this.overrides.filter(o => o.pattern !== pattern);
68
+ if (this.overrides.length !== before) {
69
+ await this.save();
70
+ return true;
71
+ }
72
+ return false;
73
+ }
74
+ /**
75
+ * Check if a pattern is overridden.
76
+ */
77
+ isOverridden(name) {
78
+ for (const override of this.overrides) {
79
+ if (this.isExpired(override))
80
+ continue;
81
+ // Exact match
82
+ if (override.pattern === name) {
83
+ return override;
84
+ }
85
+ // Glob match
86
+ if (override.pattern.includes('*')) {
87
+ const regex = new RegExp('^' + override.pattern.replace(/\*/g, '.*') + '$');
88
+ if (regex.test(name)) {
89
+ return override;
90
+ }
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ /**
96
+ * Get all active overrides.
97
+ */
98
+ getActiveOverrides() {
99
+ return this.overrides.filter(o => !this.isExpired(o));
100
+ }
101
+ /**
102
+ * Get all expired overrides.
103
+ */
104
+ getExpiredOverrides() {
105
+ return this.overrides.filter(o => this.isExpired(o));
106
+ }
107
+ /**
108
+ * Clean up expired overrides.
109
+ */
110
+ async cleanupExpired() {
111
+ const before = this.overrides.length;
112
+ this.overrides = this.overrides.filter(o => !this.isExpired(o));
113
+ if (this.overrides.length !== before) {
114
+ await this.save();
115
+ }
116
+ return before - this.overrides.length;
117
+ }
118
+ /**
119
+ * Parse inline override comments from code.
120
+ */
121
+ parseInlineOverrides(code, filePath) {
122
+ const overrides = [];
123
+ const lines = code.split('\n');
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const line = lines[i];
126
+ // Match: // rigour-allow: pattern-name
127
+ // Or: // rigour-allow: pattern-name (reason)
128
+ const match = line.match(/\/\/\s*rigour-allow:\s*(\S+)(?:\s*\(([^)]+)\))?/i);
129
+ if (match) {
130
+ overrides.push({
131
+ pattern: match[1],
132
+ reason: match[2] || `Inline override in ${filePath}:${i + 1}`,
133
+ createdAt: new Date().toISOString(),
134
+ approvedBy: `inline:${filePath}:${i + 1}`
135
+ });
136
+ }
137
+ }
138
+ return overrides;
139
+ }
140
+ /**
141
+ * Check if an override is expired.
142
+ */
143
+ isExpired(override) {
144
+ if (!override.expiresAt)
145
+ return false;
146
+ return new Date(override.expiresAt) < new Date();
147
+ }
148
+ /**
149
+ * Get default expiration date.
150
+ */
151
+ getDefaultExpiration() {
152
+ const date = new Date();
153
+ date.setDate(date.getDate() + DEFAULT_EXPIRATION_DAYS);
154
+ return date.toISOString();
155
+ }
156
+ }
157
+ /**
158
+ * Load overrides from rigour.config.yaml.
159
+ */
160
+ export async function loadConfigOverrides(rootDir) {
161
+ const configPaths = [
162
+ path.join(rootDir, 'rigour.config.yaml'),
163
+ path.join(rootDir, 'rigour.config.yml'),
164
+ path.join(rootDir, '.rigour', 'config.yaml')
165
+ ];
166
+ for (const configPath of configPaths) {
167
+ try {
168
+ const content = await fs.readFile(configPath, 'utf-8');
169
+ const { parse } = await import('yaml');
170
+ const config = parse(content);
171
+ if (config.patterns?.allow && Array.isArray(config.patterns.allow)) {
172
+ return config.patterns.allow.map((item) => {
173
+ if (typeof item === 'string') {
174
+ return {
175
+ pattern: item,
176
+ reason: 'Configured in rigour.config.yaml',
177
+ createdAt: new Date().toISOString()
178
+ };
179
+ }
180
+ return {
181
+ pattern: item.pattern || item.path || item.name,
182
+ reason: item.reason || 'Configured in rigour.config.yaml',
183
+ expiresAt: item.expires,
184
+ approvedBy: item.approved_by,
185
+ createdAt: new Date().toISOString()
186
+ };
187
+ });
188
+ }
189
+ }
190
+ catch {
191
+ // Config file doesn't exist or is invalid
192
+ continue;
193
+ }
194
+ }
195
+ return [];
196
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Security Detector
3
+ *
4
+ * Detects CVEs and security vulnerabilities in the project's dependencies
5
+ * and alerts the AI/Editor before code is written.
6
+ */
7
+ import type { SecurityResult } from './types.js';
8
+ export declare class SecurityDetector {
9
+ private rootDir;
10
+ private cachePath;
11
+ private CACHE_TTL;
12
+ constructor(rootDir: string);
13
+ /**
14
+ * Run a live security audit using NPM.
15
+ * This provides the latest CVE info from the NPM registry.
16
+ */
17
+ runAudit(): Promise<SecurityResult>;
18
+ /**
19
+ * Get a quick summary for the AI context.
20
+ */
21
+ getSecuritySummary(): Promise<string>;
22
+ private getLockfileHash;
23
+ private getCachedResult;
24
+ private saveCache;
25
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Security Detector
3
+ *
4
+ * Detects CVEs and security vulnerabilities in the project's dependencies
5
+ * and alerts the AI/Editor before code is written.
6
+ */
7
+ import { execa } from 'execa';
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+ import { createHash } from 'crypto';
11
+ export class SecurityDetector {
12
+ rootDir;
13
+ cachePath;
14
+ CACHE_TTL = 3600000; // 1 hour in milliseconds
15
+ constructor(rootDir) {
16
+ this.rootDir = rootDir;
17
+ this.cachePath = path.join(rootDir, '.rigour', 'security-cache.json');
18
+ }
19
+ /**
20
+ * Run a live security audit using NPM.
21
+ * This provides the latest CVE info from the NPM registry.
22
+ */
23
+ async runAudit() {
24
+ try {
25
+ const lockfileHash = await this.getLockfileHash();
26
+ const cached = await this.getCachedResult(lockfileHash);
27
+ if (cached) {
28
+ return cached;
29
+ }
30
+ // Run npm audit --json for machine-readable CVE data
31
+ const { stdout } = await execa('npm', ['audit', '--json'], {
32
+ cwd: this.rootDir,
33
+ reject: false // npm audit returns non-zero for found vulnerabilities
34
+ });
35
+ const auditData = JSON.parse(stdout);
36
+ const vulnerabilities = [];
37
+ if (auditData.vulnerabilities) {
38
+ for (const [name, vuln] of Object.entries(auditData.vulnerabilities)) {
39
+ const v = vuln;
40
+ // Dig into the advisory data
41
+ const via = v.via && Array.isArray(v.via) ? v.via[0] : null;
42
+ vulnerabilities.push({
43
+ cveId: via?.name || 'N/A',
44
+ packageName: name,
45
+ vulnerableRange: v.range,
46
+ severity: v.severity,
47
+ title: via?.title || `Vulnerability in ${name}`,
48
+ url: via?.url || `https://www.npmjs.com/package/${name}/vulnerability`,
49
+ currentVersion: v.nodes && v.nodes[0] ? v.version : undefined
50
+ });
51
+ }
52
+ }
53
+ const result = {
54
+ status: vulnerabilities.length > 0 ? 'VULNERABLE' : 'SECURE',
55
+ vulnerabilities: vulnerabilities.sort((a, b) => {
56
+ const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 };
57
+ return severityOrder[a.severity] - severityOrder[b.severity];
58
+ })
59
+ };
60
+ // Save to cache
61
+ await this.saveCache(lockfileHash, result);
62
+ return result;
63
+ }
64
+ catch (error) {
65
+ console.error('Security audit failed:', error);
66
+ return { status: 'SECURE', vulnerabilities: [] };
67
+ }
68
+ }
69
+ /**
70
+ * Get a quick summary for the AI context.
71
+ */
72
+ async getSecuritySummary() {
73
+ const result = await this.runAudit();
74
+ if (result.status === 'SECURE')
75
+ return '✅ No known vulnerabilities found in dependencies.';
76
+ const topVulns = result.vulnerabilities.slice(0, 3);
77
+ let summary = `⚠️ FOUND ${result.vulnerabilities.length} VULNERABILITIES:\n`;
78
+ for (const v of topVulns) {
79
+ summary += `- [${v.severity.toUpperCase()}] ${v.packageName}: ${v.title} (${v.url})\n`;
80
+ }
81
+ if (result.vulnerabilities.length > 3) {
82
+ summary += `- ...and ${result.vulnerabilities.length - 3} more. Run 'rigour check' for full report.`;
83
+ }
84
+ return summary;
85
+ }
86
+ async getLockfileHash() {
87
+ const lockfiles = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
88
+ for (const file of lockfiles) {
89
+ try {
90
+ const content = await fs.readFile(path.join(this.rootDir, file), 'utf-8');
91
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ }
97
+ return 'no-lockfile';
98
+ }
99
+ async getCachedResult(currentHash) {
100
+ try {
101
+ const content = await fs.readFile(this.cachePath, 'utf-8');
102
+ const cache = JSON.parse(content);
103
+ const isExpired = Date.now() - new Date(cache.timestamp).getTime() > this.CACHE_TTL;
104
+ if (!isExpired && cache.lockfileHash === currentHash) {
105
+ return cache.result;
106
+ }
107
+ }
108
+ catch {
109
+ // No cache or invalid cache
110
+ }
111
+ return null;
112
+ }
113
+ async saveCache(hash, result) {
114
+ try {
115
+ await fs.mkdir(path.dirname(this.cachePath), { recursive: true });
116
+ const cache = {
117
+ lockfileHash: hash,
118
+ timestamp: new Date().toISOString(),
119
+ result
120
+ };
121
+ await fs.writeFile(this.cachePath, JSON.stringify(cache, null, 2), 'utf-8');
122
+ }
123
+ catch (error) {
124
+ console.warn('Failed to save security cache:', error);
125
+ }
126
+ }
127
+ }