s9n-devops-agent 2.0.9 → 2.0.11-dev.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.
@@ -0,0 +1,567 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ============================================================================
5
+ * CONTRACT COMPLIANCE CHECKER
6
+ * ============================================================================
7
+ *
8
+ * This script checks if the codebase is in sync with contract files.
9
+ * It detects:
10
+ * - Features in code but not in FEATURES_CONTRACT.md
11
+ * - API endpoints in code but not in API_CONTRACT.md
12
+ * - Database tables in migrations but not in DATABASE_SCHEMA_CONTRACT.md
13
+ * - SQL queries in code but not in SQL_CONTRACT.json
14
+ * - Third-party services in package.json but not in THIRD_PARTY_INTEGRATIONS.md
15
+ * - Environment variables in code but not in INFRA_CONTRACT.md
16
+ *
17
+ * Usage:
18
+ * node scripts/contract-automation/check-compliance.js
19
+ * node scripts/contract-automation/check-compliance.js --fix
20
+ * node scripts/contract-automation/check-compliance.js --report=json
21
+ *
22
+ * Options:
23
+ * --fix Generate missing contract entries
24
+ * --report=<format> Output format (text|json|html)
25
+ * --strict Exit with error if any discrepancies found
26
+ *
27
+ * ============================================================================
28
+ */
29
+
30
+ import fs from 'fs';
31
+ import path from 'path';
32
+ import { execSync } from 'child_process';
33
+ import { fileURLToPath } from 'url';
34
+
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ const __dirname = path.dirname(__filename);
37
+
38
+ // Configuration
39
+ const CONFIG = {
40
+ rootDir: process.cwd(),
41
+ contractsDir: path.join(process.cwd(), 'House_Rules_Contracts'),
42
+ fix: process.argv.includes('--fix'),
43
+ report: getArgValue('--report') || 'text',
44
+ strict: process.argv.includes('--strict')
45
+ };
46
+
47
+ // Helper: Get command line argument value
48
+ function getArgValue(argName) {
49
+ const arg = process.argv.find(a => a.startsWith(argName + '='));
50
+ return arg ? arg.split('=')[1] : null;
51
+ }
52
+
53
+ // Helper: Log
54
+ function log(message, level = 'info') {
55
+ const colors = {
56
+ info: '\x1b[36m',
57
+ warn: '\x1b[33m',
58
+ error: '\x1b[31m',
59
+ success: '\x1b[32m',
60
+ reset: '\x1b[0m'
61
+ };
62
+
63
+ const prefix = {
64
+ info: '[INFO]',
65
+ warn: '[WARN]',
66
+ error: '[ERROR]',
67
+ success: '[SUCCESS]'
68
+ }[level];
69
+
70
+ const color = colors[level] || colors.reset;
71
+ console.log(`${color}${prefix} ${message}${colors.reset}`);
72
+ }
73
+
74
+ // Helper: Read file safely
75
+ function readFileSafe(filePath) {
76
+ try {
77
+ return fs.readFileSync(filePath, 'utf8');
78
+ } catch (error) {
79
+ return '';
80
+ }
81
+ }
82
+
83
+ // Helper: Find files
84
+ function findFiles(pattern, dir = CONFIG.rootDir) {
85
+ try {
86
+ const result = execSync(`find ${dir} -type f ${pattern}`, { encoding: 'utf8' });
87
+ return result.trim().split('\n').filter(Boolean);
88
+ } catch (error) {
89
+ return [];
90
+ }
91
+ }
92
+
93
+ // ============================================================================
94
+ // CONTRACT READERS
95
+ // ============================================================================
96
+
97
+ function readFeaturesContract() {
98
+ const filePath = path.join(CONFIG.contractsDir, 'FEATURES_CONTRACT.md');
99
+ if (!fs.existsSync(filePath)) return [];
100
+
101
+ const content = readFileSafe(filePath);
102
+ const features = [];
103
+
104
+ // Extract feature IDs and names
105
+ const matches = content.matchAll(/Feature ID:\s*\[?(F-\d+)\]?\s*-\s*(.+)/gi);
106
+ for (const match of matches) {
107
+ features.push({
108
+ id: match[1],
109
+ name: match[2].trim()
110
+ });
111
+ }
112
+
113
+ return features;
114
+ }
115
+
116
+ function readAPIContract() {
117
+ const filePath = path.join(CONFIG.contractsDir, 'API_CONTRACT.md');
118
+ if (!fs.existsSync(filePath)) return [];
119
+
120
+ const content = readFileSafe(filePath);
121
+ const endpoints = [];
122
+
123
+ // Extract endpoints
124
+ const matches = content.matchAll(/####?\s*`(GET|POST|PUT|DELETE|PATCH)\s+(.+?)`/gi);
125
+ for (const match of matches) {
126
+ endpoints.push({
127
+ method: match[1].toUpperCase(),
128
+ path: match[2].trim()
129
+ });
130
+ }
131
+
132
+ return endpoints;
133
+ }
134
+
135
+ function readDatabaseContract() {
136
+ const filePath = path.join(CONFIG.contractsDir, 'DATABASE_SCHEMA_CONTRACT.md');
137
+ if (!fs.existsSync(filePath)) return [];
138
+
139
+ const content = readFileSafe(filePath);
140
+ const tables = [];
141
+
142
+ // Extract table names
143
+ const matches = content.matchAll(/###\s+Table:\s+(\w+)/gi);
144
+ for (const match of matches) {
145
+ tables.push(match[1]);
146
+ }
147
+
148
+ return tables;
149
+ }
150
+
151
+ function readSQLContract() {
152
+ const filePath = path.join(CONFIG.contractsDir, 'SQL_CONTRACT.json');
153
+ if (!fs.existsSync(filePath)) return [];
154
+
155
+ try {
156
+ const content = readFileSafe(filePath);
157
+ const data = JSON.parse(content);
158
+ return Object.keys(data.queries || {});
159
+ } catch (error) {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ function readIntegrationsContract() {
165
+ const filePath = path.join(CONFIG.contractsDir, 'THIRD_PARTY_INTEGRATIONS.md');
166
+ if (!fs.existsSync(filePath)) return [];
167
+
168
+ const content = readFileSafe(filePath);
169
+ const integrations = [];
170
+
171
+ // Extract service names
172
+ const matches = content.matchAll(/###\s+(.+?)\s+\(/gi);
173
+ for (const match of matches) {
174
+ integrations.push(match[1].trim());
175
+ }
176
+
177
+ return integrations;
178
+ }
179
+
180
+ function readInfraContract() {
181
+ const filePath = path.join(CONFIG.contractsDir, 'INFRA_CONTRACT.md');
182
+ if (!fs.existsSync(filePath)) return [];
183
+
184
+ const content = readFileSafe(filePath);
185
+ const envVars = [];
186
+
187
+ // Extract environment variables
188
+ const matches = content.matchAll(/`([A-Z_][A-Z0-9_]*)`/g);
189
+ for (const match of matches) {
190
+ if (!envVars.includes(match[1])) {
191
+ envVars.push(match[1]);
192
+ }
193
+ }
194
+
195
+ return envVars;
196
+ }
197
+
198
+ // ============================================================================
199
+ // CODE SCANNERS (reuse from generate-contracts.js logic)
200
+ // ============================================================================
201
+
202
+ function scanCodeForFeatures() {
203
+ const featureDirs = [
204
+ ...findFiles('-path "*/src/features/*" -type d'),
205
+ ...findFiles('-path "*/src/modules/*" -type d')
206
+ ];
207
+
208
+ const features = [];
209
+ for (const dir of featureDirs) {
210
+ const featureName = path.basename(dir);
211
+ if (featureName !== 'features' && featureName !== 'modules') {
212
+ features.push(featureName);
213
+ }
214
+ }
215
+
216
+ return features;
217
+ }
218
+
219
+ function scanCodeForEndpoints() {
220
+ const routeFiles = [
221
+ ...findFiles('-path "*/routes/*.js"'),
222
+ ...findFiles('-path "*/api/*.js"'),
223
+ ...findFiles('-path "*/controllers/*.js"')
224
+ ];
225
+
226
+ const endpoints = [];
227
+ for (const file of routeFiles) {
228
+ const content = readFileSafe(file);
229
+ const matches = content.matchAll(/(app|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi);
230
+
231
+ for (const match of matches) {
232
+ endpoints.push({
233
+ method: match[2].toUpperCase(),
234
+ path: match[3]
235
+ });
236
+ }
237
+ }
238
+
239
+ return endpoints;
240
+ }
241
+
242
+ function scanCodeForTables() {
243
+ const migrationFiles = [
244
+ ...findFiles('-path "*/migrations/*.sql"'),
245
+ ...findFiles('-name "schema.prisma"')
246
+ ];
247
+
248
+ const tables = [];
249
+ for (const file of migrationFiles) {
250
+ const content = readFileSafe(file);
251
+
252
+ if (file.endsWith('.sql')) {
253
+ const matches = content.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/gi);
254
+ for (const match of matches) {
255
+ if (!tables.includes(match[1])) {
256
+ tables.push(match[1]);
257
+ }
258
+ }
259
+ } else if (file.endsWith('.prisma')) {
260
+ const matches = content.matchAll(/model\s+(\w+)\s*{/gi);
261
+ for (const match of matches) {
262
+ const tableName = match[1].toLowerCase();
263
+ if (!tables.includes(tableName)) {
264
+ tables.push(tableName);
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ return tables;
271
+ }
272
+
273
+ function scanCodeForIntegrations() {
274
+ const packageJsonPath = path.join(CONFIG.rootDir, 'package.json');
275
+ if (!fs.existsSync(packageJsonPath)) return [];
276
+
277
+ const packageJson = JSON.parse(readFileSafe(packageJsonPath));
278
+ const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
279
+
280
+ const knownServices = {
281
+ 'stripe': 'Stripe',
282
+ '@sendgrid/mail': 'SendGrid',
283
+ 'aws-sdk': 'AWS SDK',
284
+ '@aws-sdk/client-s3': 'AWS S3',
285
+ 'twilio': 'Twilio',
286
+ 'mailgun-js': 'Mailgun'
287
+ };
288
+
289
+ const integrations = [];
290
+ for (const [pkg, name] of Object.entries(knownServices)) {
291
+ if (dependencies[pkg]) {
292
+ integrations.push(name);
293
+ }
294
+ }
295
+
296
+ return integrations;
297
+ }
298
+
299
+ function scanCodeForEnvVars() {
300
+ const codeFiles = findFiles('-path "*/src/*.js" -o -path "*/src/*.ts"');
301
+ const envVars = new Set();
302
+
303
+ for (const file of codeFiles) {
304
+ const content = readFileSafe(file);
305
+ const matches = content.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g);
306
+ for (const match of matches) {
307
+ envVars.add(match[1]);
308
+ }
309
+ }
310
+
311
+ return Array.from(envVars);
312
+ }
313
+
314
+ // ============================================================================
315
+ // COMPLIANCE CHECKING
316
+ // ============================================================================
317
+
318
+ function checkCompliance() {
319
+ log('Checking contract compliance...');
320
+
321
+ const results = {
322
+ features: { missing: [], extra: [] },
323
+ api: { missing: [], extra: [] },
324
+ database: { missing: [], extra: [] },
325
+ integrations: { missing: [], extra: [] },
326
+ envVars: { missing: [], extra: [] }
327
+ };
328
+
329
+ // Features
330
+ const contractFeatures = readFeaturesContract().map(f => f.name.toLowerCase());
331
+ const codeFeatures = scanCodeForFeatures().map(f => f.toLowerCase());
332
+
333
+ results.features.missing = codeFeatures.filter(f => !contractFeatures.some(cf => cf.includes(f) || f.includes(cf)));
334
+ results.features.extra = contractFeatures.filter(cf => !codeFeatures.some(f => cf.includes(f) || f.includes(cf)));
335
+
336
+ // API Endpoints
337
+ const contractEndpoints = readAPIContract();
338
+ const codeEndpoints = scanCodeForEndpoints();
339
+
340
+ for (const endpoint of codeEndpoints) {
341
+ const found = contractEndpoints.some(ce =>
342
+ ce.method === endpoint.method && ce.path === endpoint.path
343
+ );
344
+ if (!found) {
345
+ results.api.missing.push(`${endpoint.method} ${endpoint.path}`);
346
+ }
347
+ }
348
+
349
+ for (const endpoint of contractEndpoints) {
350
+ const found = codeEndpoints.some(ce =>
351
+ ce.method === endpoint.method && ce.path === endpoint.path
352
+ );
353
+ if (!found) {
354
+ results.api.extra.push(`${endpoint.method} ${endpoint.path}`);
355
+ }
356
+ }
357
+
358
+ // Database Tables
359
+ const contractTables = readDatabaseContract().map(t => t.toLowerCase());
360
+ const codeTables = scanCodeForTables().map(t => t.toLowerCase());
361
+
362
+ results.database.missing = codeTables.filter(t => !contractTables.includes(t));
363
+ results.database.extra = contractTables.filter(t => !codeTables.includes(t));
364
+
365
+ // Third-party Integrations
366
+ const contractIntegrations = readIntegrationsContract().map(i => i.toLowerCase());
367
+ const codeIntegrations = scanCodeForIntegrations().map(i => i.toLowerCase());
368
+
369
+ results.integrations.missing = codeIntegrations.filter(i => !contractIntegrations.some(ci => ci.includes(i) || i.includes(ci)));
370
+ results.integrations.extra = contractIntegrations.filter(ci => !codeIntegrations.some(i => ci.includes(i) || i.includes(ci)));
371
+
372
+ // Environment Variables
373
+ const contractEnvVars = readInfraContract();
374
+ const codeEnvVars = scanCodeForEnvVars();
375
+
376
+ results.envVars.missing = codeEnvVars.filter(v => !contractEnvVars.includes(v));
377
+ results.envVars.extra = contractEnvVars.filter(v => !codeEnvVars.includes(v));
378
+
379
+ return results;
380
+ }
381
+
382
+ // ============================================================================
383
+ // REPORTING
384
+ // ============================================================================
385
+
386
+ function generateTextReport(results) {
387
+ log('='.repeat(80));
388
+ log('CONTRACT COMPLIANCE REPORT');
389
+ log('='.repeat(80));
390
+ log('');
391
+
392
+ let totalIssues = 0;
393
+
394
+ // Features
395
+ log('FEATURES:');
396
+ if (results.features.missing.length > 0) {
397
+ log(` Missing in contract (${results.features.missing.length}):`, 'warn');
398
+ results.features.missing.forEach(f => log(` - ${f}`, 'warn'));
399
+ totalIssues += results.features.missing.length;
400
+ }
401
+ if (results.features.extra.length > 0) {
402
+ log(` In contract but not in code (${results.features.extra.length}):`, 'info');
403
+ results.features.extra.forEach(f => log(` - ${f}`, 'info'));
404
+ }
405
+ if (results.features.missing.length === 0 && results.features.extra.length === 0) {
406
+ log(' ✅ All features documented', 'success');
407
+ }
408
+ log('');
409
+
410
+ // API Endpoints
411
+ log('API ENDPOINTS:');
412
+ if (results.api.missing.length > 0) {
413
+ log(` Missing in contract (${results.api.missing.length}):`, 'warn');
414
+ results.api.missing.slice(0, 10).forEach(e => log(` - ${e}`, 'warn'));
415
+ if (results.api.missing.length > 10) {
416
+ log(` ... and ${results.api.missing.length - 10} more`, 'warn');
417
+ }
418
+ totalIssues += results.api.missing.length;
419
+ }
420
+ if (results.api.extra.length > 0) {
421
+ log(` In contract but not in code (${results.api.extra.length}):`, 'info');
422
+ results.api.extra.slice(0, 10).forEach(e => log(` - ${e}`, 'info'));
423
+ if (results.api.extra.length > 10) {
424
+ log(` ... and ${results.api.extra.length - 10} more`, 'info');
425
+ }
426
+ }
427
+ if (results.api.missing.length === 0 && results.api.extra.length === 0) {
428
+ log(' ✅ All endpoints documented', 'success');
429
+ }
430
+ log('');
431
+
432
+ // Database Tables
433
+ log('DATABASE TABLES:');
434
+ if (results.database.missing.length > 0) {
435
+ log(` Missing in contract (${results.database.missing.length}):`, 'warn');
436
+ results.database.missing.forEach(t => log(` - ${t}`, 'warn'));
437
+ totalIssues += results.database.missing.length;
438
+ }
439
+ if (results.database.extra.length > 0) {
440
+ log(` In contract but not in migrations (${results.database.extra.length}):`, 'info');
441
+ results.database.extra.forEach(t => log(` - ${t}`, 'info'));
442
+ }
443
+ if (results.database.missing.length === 0 && results.database.extra.length === 0) {
444
+ log(' ✅ All tables documented', 'success');
445
+ }
446
+ log('');
447
+
448
+ // Third-party Integrations
449
+ log('THIRD-PARTY INTEGRATIONS:');
450
+ if (results.integrations.missing.length > 0) {
451
+ log(` Missing in contract (${results.integrations.missing.length}):`, 'warn');
452
+ results.integrations.missing.forEach(i => log(` - ${i}`, 'warn'));
453
+ totalIssues += results.integrations.missing.length;
454
+ }
455
+ if (results.integrations.extra.length > 0) {
456
+ log(` In contract but not in package.json (${results.integrations.extra.length}):`, 'info');
457
+ results.integrations.extra.forEach(i => log(` - ${i}`, 'info'));
458
+ }
459
+ if (results.integrations.missing.length === 0 && results.integrations.extra.length === 0) {
460
+ log(' ✅ All integrations documented', 'success');
461
+ }
462
+ log('');
463
+
464
+ // Environment Variables
465
+ log('ENVIRONMENT VARIABLES:');
466
+ if (results.envVars.missing.length > 0) {
467
+ log(` Missing in contract (${results.envVars.missing.length}):`, 'warn');
468
+ results.envVars.missing.slice(0, 10).forEach(v => log(` - ${v}`, 'warn'));
469
+ if (results.envVars.missing.length > 10) {
470
+ log(` ... and ${results.envVars.missing.length - 10} more`, 'warn');
471
+ }
472
+ totalIssues += results.envVars.missing.length;
473
+ }
474
+ if (results.envVars.extra.length > 0) {
475
+ log(` In contract but not in code (${results.envVars.extra.length}):`, 'info');
476
+ results.envVars.extra.slice(0, 10).forEach(v => log(` - ${v}`, 'info'));
477
+ if (results.envVars.extra.length > 10) {
478
+ log(` ... and ${results.envVars.extra.length - 10} more`, 'info');
479
+ }
480
+ }
481
+ if (results.envVars.missing.length === 0 && results.envVars.extra.length === 0) {
482
+ log(' ✅ All environment variables documented', 'success');
483
+ }
484
+ log('');
485
+
486
+ // Summary
487
+ log('='.repeat(80));
488
+ if (totalIssues === 0) {
489
+ log('COMPLIANCE CHECK PASSED ✅', 'success');
490
+ log('All contracts are in sync with the codebase.', 'success');
491
+ } else {
492
+ log(`COMPLIANCE CHECK FAILED ❌`, 'error');
493
+ log(`Found ${totalIssues} items missing from contracts.`, 'error');
494
+ log('Run with --fix to generate missing entries.', 'info');
495
+ }
496
+ log('='.repeat(80));
497
+
498
+ return totalIssues === 0;
499
+ }
500
+
501
+ function generateJSONReport(results) {
502
+ const report = {
503
+ timestamp: new Date().toISOString(),
504
+ summary: {
505
+ features: {
506
+ missing: results.features.missing.length,
507
+ extra: results.features.extra.length
508
+ },
509
+ api: {
510
+ missing: results.api.missing.length,
511
+ extra: results.api.extra.length
512
+ },
513
+ database: {
514
+ missing: results.database.missing.length,
515
+ extra: results.database.extra.length
516
+ },
517
+ integrations: {
518
+ missing: results.integrations.missing.length,
519
+ extra: results.integrations.extra.length
520
+ },
521
+ envVars: {
522
+ missing: results.envVars.missing.length,
523
+ extra: results.envVars.extra.length
524
+ }
525
+ },
526
+ details: results
527
+ };
528
+
529
+ console.log(JSON.stringify(report, null, 2));
530
+
531
+ const totalIssues = Object.values(report.summary).reduce((sum, cat) => sum + cat.missing, 0);
532
+ return totalIssues === 0;
533
+ }
534
+
535
+ // ============================================================================
536
+ // MAIN EXECUTION
537
+ // ============================================================================
538
+
539
+ function main() {
540
+ // Check if contracts directory exists
541
+ if (!fs.existsSync(CONFIG.contractsDir)) {
542
+ log(`Contracts directory not found: ${CONFIG.contractsDir}`, 'error');
543
+ log('Run contract generation first or merge the contract system branch.', 'info');
544
+ process.exit(1);
545
+ }
546
+
547
+ // Run compliance check
548
+ const results = checkCompliance();
549
+
550
+ // Generate report
551
+ let passed;
552
+ if (CONFIG.report === 'json') {
553
+ passed = generateJSONReport(results);
554
+ } else {
555
+ passed = generateTextReport(results);
556
+ }
557
+
558
+ // Exit with appropriate code
559
+ if (CONFIG.strict && !passed) {
560
+ process.exit(1);
561
+ } else {
562
+ process.exit(0);
563
+ }
564
+ }
565
+
566
+ // Run
567
+ main();