@intentsolutionsio/data-validation-engine 1.0.0 → 1.0.2
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/package.json +1 -1
- package/skills/validating-database-integrity/SKILL.md +13 -6
- package/skills/validating-database-integrity/references/README.md +0 -1
- package/skills/validating-database-integrity/scripts/configure_validation_rules.py +44 -74
- package/skills/validating-database-integrity/scripts/generate_validation_report.py +58 -70
package/package.json
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: validating-database-integrity
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
description: 'Process use when you need to ensure database integrity through comprehensive
|
|
4
|
+
data validation.
|
|
5
|
+
|
|
6
|
+
This skill validates data types, ranges, formats, referential integrity, and business
|
|
7
|
+
rules.
|
|
8
|
+
|
|
6
9
|
Trigger with phrases like "validate database data", "implement data validation rules",
|
|
10
|
+
|
|
7
11
|
"enforce data integrity constraints", or "validate data formats".
|
|
8
12
|
|
|
13
|
+
'
|
|
9
14
|
allowed-tools: Read, Write, Edit, Grep, Glob, Bash(psql:*), Bash(mysql:*)
|
|
10
15
|
version: 1.0.0
|
|
11
16
|
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
12
17
|
license: MIT
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
tags:
|
|
19
|
+
- database
|
|
20
|
+
- validating-database
|
|
21
|
+
compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
|
|
15
22
|
---
|
|
16
23
|
# Data Validation Engine
|
|
17
24
|
|
|
@@ -97,4 +104,4 @@ Implement and enforce data integrity rules at the database level using CHECK con
|
|
|
97
104
|
- PostgreSQL triggers: https://www.postgresql.org/docs/current/triggers.html
|
|
98
105
|
- MySQL CHECK constraints (8.0.16+): https://dev.mysql.com/doc/refman/8.0/en/create-table-check-constraints.html
|
|
99
106
|
- Data validation patterns: https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS
|
|
100
|
-
- NOT VALID constraint option: https://www.postgresql.org/docs/current/sql-altertable.html
|
|
107
|
+
- NOT VALID constraint option: https://www.postgresql.org/docs/current/sql-altertable.html
|
|
@@ -10,8 +10,7 @@ import argparse
|
|
|
10
10
|
import json
|
|
11
11
|
import sys
|
|
12
12
|
from datetime import datetime
|
|
13
|
-
from
|
|
14
|
-
from typing import Dict, List, Any, Optional
|
|
13
|
+
from typing import Dict, Any
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class ValidationRuleConfigurator:
|
|
@@ -30,11 +29,9 @@ class ValidationRuleConfigurator:
|
|
|
30
29
|
Args:
|
|
31
30
|
column: Column name
|
|
32
31
|
"""
|
|
33
|
-
self.rules.append(
|
|
34
|
-
"rule": "not_null",
|
|
35
|
-
|
|
36
|
-
"description": f"Column {column} must not contain NULL values"
|
|
37
|
-
})
|
|
32
|
+
self.rules.append(
|
|
33
|
+
{"rule": "not_null", "column": column, "description": f"Column {column} must not contain NULL values"}
|
|
34
|
+
)
|
|
38
35
|
|
|
39
36
|
def add_unique_rule(self, column: str):
|
|
40
37
|
"""
|
|
@@ -43,18 +40,11 @@ class ValidationRuleConfigurator:
|
|
|
43
40
|
Args:
|
|
44
41
|
column: Column name
|
|
45
42
|
"""
|
|
46
|
-
self.rules.append(
|
|
47
|
-
"rule": "unique",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def add_range_rule(
|
|
53
|
-
self,
|
|
54
|
-
column: str,
|
|
55
|
-
min_value: float,
|
|
56
|
-
max_value: float
|
|
57
|
-
):
|
|
43
|
+
self.rules.append(
|
|
44
|
+
{"rule": "unique", "column": column, "description": f"Column {column} must contain unique values"}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def add_range_rule(self, column: str, min_value: float, max_value: float):
|
|
58
48
|
"""
|
|
59
49
|
Add a RANGE validation rule.
|
|
60
50
|
|
|
@@ -63,13 +53,15 @@ class ValidationRuleConfigurator:
|
|
|
63
53
|
min_value: Minimum allowed value
|
|
64
54
|
max_value: Maximum allowed value
|
|
65
55
|
"""
|
|
66
|
-
self.rules.append(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
self.rules.append(
|
|
57
|
+
{
|
|
58
|
+
"rule": "range",
|
|
59
|
+
"column": column,
|
|
60
|
+
"min": min_value,
|
|
61
|
+
"max": max_value,
|
|
62
|
+
"description": f"Column {column} values must be between {min_value} and {max_value}",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
73
65
|
|
|
74
66
|
def add_pattern_rule(self, column: str, pattern: str):
|
|
75
67
|
"""
|
|
@@ -79,12 +71,14 @@ class ValidationRuleConfigurator:
|
|
|
79
71
|
column: Column name
|
|
80
72
|
pattern: Regular expression pattern
|
|
81
73
|
"""
|
|
82
|
-
self.rules.append(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
self.rules.append(
|
|
75
|
+
{
|
|
76
|
+
"rule": "pattern",
|
|
77
|
+
"column": column,
|
|
78
|
+
"pattern": pattern,
|
|
79
|
+
"description": f"Column {column} values must match pattern: {pattern}",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
88
82
|
|
|
89
83
|
def add_custom_rule(self, column: str, query: str):
|
|
90
84
|
"""
|
|
@@ -94,12 +88,9 @@ class ValidationRuleConfigurator:
|
|
|
94
88
|
column: Column name
|
|
95
89
|
query: Custom SQL query
|
|
96
90
|
"""
|
|
97
|
-
self.rules.append(
|
|
98
|
-
"rule": "custom",
|
|
99
|
-
|
|
100
|
-
"query": query,
|
|
101
|
-
"description": f"Custom validation on {column}"
|
|
102
|
-
})
|
|
91
|
+
self.rules.append(
|
|
92
|
+
{"rule": "custom", "column": column, "query": query, "description": f"Custom validation on {column}"}
|
|
93
|
+
)
|
|
103
94
|
|
|
104
95
|
def remove_rule(self, index: int) -> bool:
|
|
105
96
|
"""
|
|
@@ -127,7 +118,7 @@ class ValidationRuleConfigurator:
|
|
|
127
118
|
"table": self.table_name,
|
|
128
119
|
"database": self.database,
|
|
129
120
|
"created_at": datetime.now().isoformat(),
|
|
130
|
-
"validations": self.rules
|
|
121
|
+
"validations": self.rules,
|
|
131
122
|
}
|
|
132
123
|
|
|
133
124
|
def load_config(self, filepath: str) -> bool:
|
|
@@ -141,7 +132,7 @@ class ValidationRuleConfigurator:
|
|
|
141
132
|
True if successful, False otherwise
|
|
142
133
|
"""
|
|
143
134
|
try:
|
|
144
|
-
with open(filepath,
|
|
135
|
+
with open(filepath, "r") as f:
|
|
145
136
|
config = json.load(f)
|
|
146
137
|
|
|
147
138
|
self.table_name = config.get("table", "")
|
|
@@ -164,7 +155,7 @@ class ValidationRuleConfigurator:
|
|
|
164
155
|
True if successful, False otherwise
|
|
165
156
|
"""
|
|
166
157
|
try:
|
|
167
|
-
with open(filepath,
|
|
158
|
+
with open(filepath, "w") as f:
|
|
168
159
|
json.dump(self.get_config_dict(), f, indent=2)
|
|
169
160
|
return True
|
|
170
161
|
except Exception as e:
|
|
@@ -179,9 +170,9 @@ def interactive_mode(configurator: ValidationRuleConfigurator):
|
|
|
179
170
|
Args:
|
|
180
171
|
configurator: ValidationRuleConfigurator instance
|
|
181
172
|
"""
|
|
182
|
-
print("\n" + "="*60)
|
|
173
|
+
print("\n" + "=" * 60)
|
|
183
174
|
print("Data Validation Rule Configurator")
|
|
184
|
-
print("="*60 + "\n")
|
|
175
|
+
print("=" * 60 + "\n")
|
|
185
176
|
|
|
186
177
|
# Get table and database info
|
|
187
178
|
configurator.table_name = input("Enter table name: ").strip()
|
|
@@ -258,9 +249,9 @@ def interactive_mode(configurator: ValidationRuleConfigurator):
|
|
|
258
249
|
print("Invalid choice. Please try again.")
|
|
259
250
|
|
|
260
251
|
# Summary and save
|
|
261
|
-
print("\n" + "="*60)
|
|
252
|
+
print("\n" + "=" * 60)
|
|
262
253
|
print("Configuration Summary")
|
|
263
|
-
print("="*60)
|
|
254
|
+
print("=" * 60)
|
|
264
255
|
print(f"Table: {configurator.table_name}")
|
|
265
256
|
print(f"Database: {configurator.database or '(none specified)'}")
|
|
266
257
|
print(f"Total Rules: {len(configurator.rules)}\n")
|
|
@@ -406,37 +397,16 @@ Examples:
|
|
|
406
397
|
|
|
407
398
|
# Load and modify existing rules
|
|
408
399
|
%(prog)s --load rules.json --not-null phone --output rules.json
|
|
409
|
-
"""
|
|
400
|
+
""",
|
|
410
401
|
)
|
|
411
402
|
|
|
412
|
-
parser.add_argument(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
parser.add_argument(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
)
|
|
420
|
-
parser.add_argument(
|
|
421
|
-
"--not-null",
|
|
422
|
-
help="Comma-separated columns that must not be NULL"
|
|
423
|
-
)
|
|
424
|
-
parser.add_argument(
|
|
425
|
-
"--unique",
|
|
426
|
-
help="Comma-separated columns that must be unique"
|
|
427
|
-
)
|
|
428
|
-
parser.add_argument(
|
|
429
|
-
"--range",
|
|
430
|
-
help="Range validations in format: col:min:max,col2:min2:max2"
|
|
431
|
-
)
|
|
432
|
-
parser.add_argument(
|
|
433
|
-
"--load",
|
|
434
|
-
help="Load existing configuration file"
|
|
435
|
-
)
|
|
436
|
-
parser.add_argument(
|
|
437
|
-
"--output",
|
|
438
|
-
help="Output file for configuration (JSON)"
|
|
439
|
-
)
|
|
403
|
+
parser.add_argument("--table", help="Table name for non-interactive mode")
|
|
404
|
+
parser.add_argument("--database", help="Database name")
|
|
405
|
+
parser.add_argument("--not-null", help="Comma-separated columns that must not be NULL")
|
|
406
|
+
parser.add_argument("--unique", help="Comma-separated columns that must be unique")
|
|
407
|
+
parser.add_argument("--range", help="Range validations in format: col:min:max,col2:min2:max2")
|
|
408
|
+
parser.add_argument("--load", help="Load existing configuration file")
|
|
409
|
+
parser.add_argument("--output", help="Output file for configuration (JSON)")
|
|
440
410
|
|
|
441
411
|
args = parser.parse_args()
|
|
442
412
|
|
|
@@ -10,8 +10,7 @@ import argparse
|
|
|
10
10
|
import json
|
|
11
11
|
import sys
|
|
12
12
|
from datetime import datetime
|
|
13
|
-
from
|
|
14
|
-
from typing import Dict, List, Any, Optional
|
|
13
|
+
from typing import Dict, Any
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class ValidationReportGenerator:
|
|
@@ -33,14 +32,14 @@ class ValidationReportGenerator:
|
|
|
33
32
|
True if successful, False otherwise
|
|
34
33
|
"""
|
|
35
34
|
try:
|
|
36
|
-
with open(filepath,
|
|
35
|
+
with open(filepath, "r") as f:
|
|
37
36
|
data = json.load(f)
|
|
38
37
|
|
|
39
38
|
self.results = data.get("validations", [])
|
|
40
39
|
self.metadata = {
|
|
41
40
|
"table": data.get("table", "unknown"),
|
|
42
41
|
"timestamp": data.get("timestamp", datetime.now().isoformat()),
|
|
43
|
-
"statistics": data.get("statistics", {})
|
|
42
|
+
"statistics": data.get("statistics", {}),
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
return True
|
|
@@ -56,13 +55,7 @@ class ValidationReportGenerator:
|
|
|
56
55
|
Dictionary with summary stats
|
|
57
56
|
"""
|
|
58
57
|
if not self.results:
|
|
59
|
-
return {
|
|
60
|
-
"total": 0,
|
|
61
|
-
"passed": 0,
|
|
62
|
-
"failed": 0,
|
|
63
|
-
"pass_rate": 0.0,
|
|
64
|
-
"issues": []
|
|
65
|
-
}
|
|
58
|
+
return {"total": 0, "passed": 0, "failed": 0, "pass_rate": 0.0, "issues": []}
|
|
66
59
|
|
|
67
60
|
total = len(self.results)
|
|
68
61
|
passed = sum(1 for r in self.results if r.get("valid", False))
|
|
@@ -72,11 +65,13 @@ class ValidationReportGenerator:
|
|
|
72
65
|
for result in self.results:
|
|
73
66
|
if not result.get("valid", False):
|
|
74
67
|
details = result.get("details", {})
|
|
75
|
-
issues.append(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
issues.append(
|
|
69
|
+
{
|
|
70
|
+
"rule": details.get("rule", "unknown"),
|
|
71
|
+
"column": details.get("column", "unknown"),
|
|
72
|
+
"severity": self._determine_severity(details.get("rule")),
|
|
73
|
+
}
|
|
74
|
+
)
|
|
80
75
|
|
|
81
76
|
return {
|
|
82
77
|
"total": total,
|
|
@@ -86,7 +81,7 @@ class ValidationReportGenerator:
|
|
|
86
81
|
"issues": issues,
|
|
87
82
|
"critical_issues": sum(1 for i in issues if i["severity"] == "critical"),
|
|
88
83
|
"high_issues": sum(1 for i in issues if i["severity"] == "high"),
|
|
89
|
-
"medium_issues": sum(1 for i in issues if i["severity"] == "medium")
|
|
84
|
+
"medium_issues": sum(1 for i in issues if i["severity"] == "medium"),
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
def _determine_severity(self, rule: str) -> str:
|
|
@@ -120,7 +115,7 @@ class ValidationReportGenerator:
|
|
|
120
115
|
"metadata": self.metadata,
|
|
121
116
|
"summary": summary,
|
|
122
117
|
"timestamp": datetime.now().isoformat(),
|
|
123
|
-
"validations": self.results
|
|
118
|
+
"validations": self.results,
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
return json.dumps(report, indent=2)
|
|
@@ -130,15 +125,15 @@ class ValidationReportGenerator:
|
|
|
130
125
|
summary = self.generate_summary()
|
|
131
126
|
|
|
132
127
|
md = []
|
|
133
|
-
md.append(
|
|
128
|
+
md.append("# Data Validation Report")
|
|
134
129
|
md.append(f"\n**Table:** {self.metadata.get('table', 'Unknown')}")
|
|
135
130
|
md.append(f"**Generated:** {datetime.now().isoformat()}")
|
|
136
131
|
md.append("")
|
|
137
132
|
|
|
138
133
|
# Summary section
|
|
139
134
|
md.append("## Executive Summary\n")
|
|
140
|
-
md.append(
|
|
141
|
-
md.append(
|
|
135
|
+
md.append("| Metric | Value |")
|
|
136
|
+
md.append("|--------|-------|")
|
|
142
137
|
md.append(f"| Total Checks | {summary['total']} |")
|
|
143
138
|
md.append(f"| Passed | {summary['passed']} |")
|
|
144
139
|
md.append(f"| Failed | {summary['failed']} |")
|
|
@@ -154,32 +149,32 @@ class ValidationReportGenerator:
|
|
|
154
149
|
md.append("")
|
|
155
150
|
|
|
156
151
|
# Issues section
|
|
157
|
-
if summary[
|
|
152
|
+
if summary["failed"] > 0:
|
|
158
153
|
md.append("## Issues Identified\n")
|
|
159
154
|
|
|
160
|
-
if summary[
|
|
155
|
+
if summary["critical_issues"] > 0:
|
|
161
156
|
md.append("### Critical Issues\n")
|
|
162
|
-
for issue in summary[
|
|
163
|
-
if issue[
|
|
157
|
+
for issue in summary["issues"]:
|
|
158
|
+
if issue["severity"] == "critical":
|
|
164
159
|
md.append(f"- **{issue['rule']}** on column `{issue['column']}`")
|
|
165
160
|
md.append("")
|
|
166
161
|
|
|
167
|
-
if summary[
|
|
162
|
+
if summary["high_issues"] > 0:
|
|
168
163
|
md.append("### High Priority Issues\n")
|
|
169
|
-
for issue in summary[
|
|
170
|
-
if issue[
|
|
164
|
+
for issue in summary["issues"]:
|
|
165
|
+
if issue["severity"] == "high":
|
|
171
166
|
md.append(f"- **{issue['rule']}** on column `{issue['column']}`")
|
|
172
167
|
md.append("")
|
|
173
168
|
|
|
174
|
-
if summary[
|
|
169
|
+
if summary["medium_issues"] > 0:
|
|
175
170
|
md.append("### Medium Priority Issues\n")
|
|
176
|
-
for issue in summary[
|
|
177
|
-
if issue[
|
|
171
|
+
for issue in summary["issues"]:
|
|
172
|
+
if issue["severity"] == "medium":
|
|
178
173
|
md.append(f"- **{issue['rule']}** on column `{issue['column']}`")
|
|
179
174
|
md.append("")
|
|
180
175
|
|
|
181
176
|
# Detailed results
|
|
182
|
-
if summary[
|
|
177
|
+
if summary["failed"] > 0:
|
|
183
178
|
md.append("## Detailed Validation Results\n")
|
|
184
179
|
for result in self.results:
|
|
185
180
|
if not result.get("valid", False):
|
|
@@ -194,19 +189,20 @@ class ValidationReportGenerator:
|
|
|
194
189
|
elif rule == "not_null":
|
|
195
190
|
md.append(f"**Issue:** Found {details.get('null_count', 0)} NULL values\n")
|
|
196
191
|
elif rule == "unique":
|
|
197
|
-
md.append(f"**Issue:** Found {details.get('duplicate_count', 0)} "
|
|
198
|
-
f"duplicate groups\n")
|
|
192
|
+
md.append(f"**Issue:** Found {details.get('duplicate_count', 0)} duplicate groups\n")
|
|
199
193
|
elif rule == "range":
|
|
200
|
-
md.append(
|
|
201
|
-
|
|
194
|
+
md.append(
|
|
195
|
+
f"**Issue:** {details.get('out_of_range_count', 0)} values "
|
|
196
|
+
f"outside range [{details.get('min')}, {details.get('max')}]\n"
|
|
197
|
+
)
|
|
202
198
|
|
|
203
199
|
# Recommendations
|
|
204
200
|
md.append("## Recommendations\n")
|
|
205
|
-
if summary[
|
|
201
|
+
if summary["critical_issues"] > 0:
|
|
206
202
|
md.append("1. **Immediately address critical issues** - These indicate data integrity problems")
|
|
207
|
-
if summary[
|
|
203
|
+
if summary["high_issues"] > 0:
|
|
208
204
|
md.append("2. **Resolve high priority issues within 1 week** - These affect data quality")
|
|
209
|
-
if summary[
|
|
205
|
+
if summary["failed"] == 0:
|
|
210
206
|
md.append("✅ **All validations passed!** - Data integrity is maintained")
|
|
211
207
|
else:
|
|
212
208
|
md.append("3. **Review validation rules** - Ensure they match business requirements")
|
|
@@ -254,18 +250,26 @@ class ValidationReportGenerator:
|
|
|
254
250
|
"</head>",
|
|
255
251
|
"<body>",
|
|
256
252
|
"<div class='container'>",
|
|
257
|
-
|
|
253
|
+
"<h1>Data Validation Report</h1>",
|
|
258
254
|
f"<p><strong>Table:</strong> {self.metadata.get('table', 'Unknown')}</p>",
|
|
259
|
-
f"<p><strong>Generated:</strong> {datetime.now().isoformat()}</p>"
|
|
255
|
+
f"<p><strong>Generated:</strong> {datetime.now().isoformat()}</p>",
|
|
260
256
|
]
|
|
261
257
|
|
|
262
258
|
# Summary metrics
|
|
263
259
|
html.append("<h2>Summary</h2>")
|
|
264
260
|
html.append("<div class='summary'>")
|
|
265
|
-
html.append(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
html.append(
|
|
261
|
+
html.append(
|
|
262
|
+
f"<div class='metric'><div class='value'>{summary['total']}</div><div class='label'>Total Checks</div></div>"
|
|
263
|
+
)
|
|
264
|
+
html.append(
|
|
265
|
+
f"<div class='metric'><div class='value success'>{summary['passed']}</div><div class='label'>Passed</div></div>"
|
|
266
|
+
)
|
|
267
|
+
html.append(
|
|
268
|
+
f"<div class='metric'><div class='value critical'>{summary['failed']}</div><div class='label'>Failed</div></div>"
|
|
269
|
+
)
|
|
270
|
+
html.append(
|
|
271
|
+
f"<div class='metric'><div class='value'>{summary['pass_rate']:.1f}%</div><div class='label'>Pass Rate</div></div>"
|
|
272
|
+
)
|
|
269
273
|
html.append("</div>")
|
|
270
274
|
|
|
271
275
|
# Table statistics
|
|
@@ -278,12 +282,12 @@ class ValidationReportGenerator:
|
|
|
278
282
|
html.append("</table>")
|
|
279
283
|
|
|
280
284
|
# Issues
|
|
281
|
-
if summary[
|
|
285
|
+
if summary["failed"] > 0:
|
|
282
286
|
html.append("<h2>Issues</h2>")
|
|
283
287
|
html.append("<div class='issue-list'>")
|
|
284
288
|
|
|
285
|
-
for issue in summary[
|
|
286
|
-
severity_class = issue[
|
|
289
|
+
for issue in summary["issues"]:
|
|
290
|
+
severity_class = issue["severity"]
|
|
287
291
|
html.append(
|
|
288
292
|
f"<div class='issue'>"
|
|
289
293
|
f"<span class='{severity_class}'>{issue['severity'].upper()}</span>: "
|
|
@@ -319,29 +323,13 @@ Examples:
|
|
|
319
323
|
%(prog)s --results validation.json --format markdown
|
|
320
324
|
%(prog)s --results validation.json --format html --output report.html
|
|
321
325
|
%(prog)s --results validation.json --format json --output report.json
|
|
322
|
-
"""
|
|
326
|
+
""",
|
|
323
327
|
)
|
|
324
328
|
|
|
325
|
-
parser.add_argument(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
)
|
|
330
|
-
parser.add_argument(
|
|
331
|
-
"--format",
|
|
332
|
-
default="markdown",
|
|
333
|
-
choices=["json", "markdown", "html"],
|
|
334
|
-
help="Report format"
|
|
335
|
-
)
|
|
336
|
-
parser.add_argument(
|
|
337
|
-
"--output",
|
|
338
|
-
help="Output file for report"
|
|
339
|
-
)
|
|
340
|
-
parser.add_argument(
|
|
341
|
-
"--verbose",
|
|
342
|
-
action="store_true",
|
|
343
|
-
help="Print detailed output"
|
|
344
|
-
)
|
|
329
|
+
parser.add_argument("--results", required=True, help="Path to JSON file containing validation results")
|
|
330
|
+
parser.add_argument("--format", default="markdown", choices=["json", "markdown", "html"], help="Report format")
|
|
331
|
+
parser.add_argument("--output", help="Output file for report")
|
|
332
|
+
parser.add_argument("--verbose", action="store_true", help="Print detailed output")
|
|
345
333
|
|
|
346
334
|
args = parser.parse_args()
|
|
347
335
|
|
|
@@ -368,7 +356,7 @@ Examples:
|
|
|
368
356
|
|
|
369
357
|
# Save to file if requested
|
|
370
358
|
if args.output:
|
|
371
|
-
with open(args.output,
|
|
359
|
+
with open(args.output, "w") as f:
|
|
372
360
|
f.write(report)
|
|
373
361
|
|
|
374
362
|
if args.verbose:
|