doclify-guardrail 1.0.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.
package/src/report.mjs ADDED
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Generate a markdown report from scan results and write it to disk.
6
+ * @param {object} output - full output object with files[], summary
7
+ * @param {object} options - { reportPath: string }
8
+ * @returns {string} - resolved path of written report
9
+ */
10
+ function generateReport(output, options) {
11
+ const lines = [];
12
+ const now = new Date();
13
+ const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
14
+
15
+ lines.push('# Doclify Guardrail Report');
16
+ lines.push('');
17
+ lines.push(`**Date:** ${dateStr} `);
18
+ lines.push(`**Files scanned:** ${output.summary.filesScanned} `);
19
+ lines.push(`**Result:** ${output.summary.status === 'PASS' ? '\u2713 PASS' : `\u2717 ${output.summary.totalErrors} errors, ${output.summary.totalWarnings} warnings`}`);
20
+ if (typeof output.summary.avgHealthScore === 'number') {
21
+ lines.push(`**Avg health score:** ${output.summary.avgHealthScore}/100`);
22
+ }
23
+ lines.push('');
24
+
25
+ // Summary table
26
+ lines.push('## Summary');
27
+ lines.push('');
28
+ lines.push('| File | Status | Errors | Warnings |');
29
+ lines.push('|------|--------|--------|----------|');
30
+
31
+ for (const f of output.files) {
32
+ const icon = f.pass ? '\u2713 PASS' : '\u2717 FAIL';
33
+ lines.push(`| ${f.file} | ${icon} | ${f.summary.errors} | ${f.summary.warnings} |`);
34
+ }
35
+ lines.push('');
36
+
37
+ // Details per file
38
+ const filesWithFindings = output.files.filter(
39
+ f => f.findings.errors.length > 0 || f.findings.warnings.length > 0
40
+ );
41
+
42
+ if (filesWithFindings.length > 0) {
43
+ lines.push('## Details');
44
+ lines.push('');
45
+
46
+ for (const f of filesWithFindings) {
47
+ lines.push(`### ${f.file}`);
48
+ lines.push('');
49
+
50
+ for (const finding of f.findings.errors) {
51
+ const lineRef = finding.line != null ? `line ${finding.line}` : '';
52
+ lines.push(`- **ERROR** ${lineRef}: ${finding.message}`);
53
+ }
54
+ for (const finding of f.findings.warnings) {
55
+ const lineRef = finding.line != null ? `line ${finding.line}` : '';
56
+ lines.push(`- **WARNING** ${lineRef}: ${finding.message}`);
57
+ }
58
+ lines.push('');
59
+ }
60
+ }
61
+
62
+ // File errors
63
+ if (output.fileErrors && output.fileErrors.length > 0) {
64
+ lines.push('## Unreadable Files');
65
+ lines.push('');
66
+ for (const fe of output.fileErrors) {
67
+ lines.push(`- \`${fe.file}\`: ${fe.error}`);
68
+ }
69
+ lines.push('');
70
+ }
71
+
72
+ // Footer
73
+ lines.push('---');
74
+ lines.push(`*Generated by Doclify Guardrail v${output.version} in ${output.summary.elapsed}s*`);
75
+ lines.push('');
76
+
77
+ const reportContent = lines.join('\n');
78
+ const resolvedPath = path.resolve(options.reportPath);
79
+ fs.writeFileSync(resolvedPath, reportContent, 'utf8');
80
+ return resolvedPath;
81
+ }
82
+
83
+ export { generateReport };
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Load and validate custom rules from a JSON file.
6
+ * @param {string} rulesPath - path to JSON rules file
7
+ * @returns {Array} - validated rules with compiled RegExp
8
+ */
9
+ function loadCustomRules(rulesPath) {
10
+ const resolved = path.resolve(rulesPath);
11
+
12
+ if (!fs.existsSync(resolved)) {
13
+ throw new Error(`Rules file not found: ${resolved}`);
14
+ }
15
+
16
+ let raw;
17
+ try {
18
+ raw = JSON.parse(fs.readFileSync(resolved, 'utf8'));
19
+ } catch (err) {
20
+ throw new Error(`Invalid JSON in rules file (${resolved}): ${err.message}`);
21
+ }
22
+
23
+ if (!raw || !Array.isArray(raw.rules)) {
24
+ throw new Error(`Rules file must contain { "rules": [...] }`);
25
+ }
26
+
27
+ return raw.rules.map((rule, index) => validateCustomRule(rule, index));
28
+ }
29
+
30
+ function validateCustomRule(rule, index) {
31
+ if (!rule.id || typeof rule.id !== 'string') {
32
+ throw new Error(`Rule at index ${index}: missing or invalid "id"`);
33
+ }
34
+ if (!rule.pattern || typeof rule.pattern !== 'string') {
35
+ throw new Error(`Rule "${rule.id}": missing or invalid "pattern"`);
36
+ }
37
+ if (!rule.message || typeof rule.message !== 'string') {
38
+ throw new Error(`Rule "${rule.id}": missing or invalid "message"`);
39
+ }
40
+
41
+ const severity = rule.severity || 'warning';
42
+ if (severity !== 'error' && severity !== 'warning') {
43
+ throw new Error(`Rule "${rule.id}": severity must be "error" or "warning"`);
44
+ }
45
+
46
+ let compiledRegex;
47
+ try {
48
+ compiledRegex = new RegExp(rule.pattern, rule.flags || 'gi');
49
+ } catch (err) {
50
+ throw new Error(`Rule "${rule.id}": invalid regex pattern: ${err.message}`);
51
+ }
52
+
53
+ return {
54
+ id: rule.id,
55
+ severity,
56
+ pattern: compiledRegex,
57
+ message: rule.message
58
+ };
59
+ }
60
+
61
+ export { loadCustomRules };