@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
1313use std:: process;
1414
1515use config:: load_config;
16- use generator:: generate_specs_for_unspecced_modules;
16+ use generator:: { generate_specs_for_unspecced_modules, generate_specs_for_unspecced_modules_paths } ;
1717use validator:: { compute_coverage, find_spec_files, get_schema_table_names, validate_spec} ;
1818
1919#[ derive( Parser ) ]
@@ -37,6 +37,10 @@ struct Cli {
3737 /// Project root directory (default: cwd)
3838 #[ arg( long, global = true ) ]
3939 root : Option < PathBuf > ,
40+
41+ /// Output results as JSON instead of colored text
42+ #[ arg( long, global = true ) ]
43+ json : bool ,
4044}
4145
4246#[ derive( Subcommand ) ]
@@ -64,9 +68,9 @@ fn main() {
6468
6569 match command {
6670 Command :: Init => cmd_init ( & root) ,
67- Command :: Check => cmd_check ( & root, cli. strict , cli. require_coverage ) ,
68- Command :: Coverage => cmd_coverage ( & root, cli. strict , cli. require_coverage ) ,
69- Command :: Generate => cmd_generate ( & root, cli. strict , cli. require_coverage ) ,
71+ Command :: Check => cmd_check ( & root, cli. strict , cli. require_coverage , cli . json ) ,
72+ Command :: Coverage => cmd_coverage ( & root, cli. strict , cli. require_coverage , cli . json ) ,
73+ Command :: Generate => cmd_generate ( & root, cli. strict , cli. require_coverage , cli . json ) ,
7074 Command :: Watch => watch:: run_watch ( & root, cli. strict , cli. require_coverage ) ,
7175 }
7276}
@@ -99,13 +103,31 @@ fn cmd_init(root: &Path) {
99103 println ! ( "{} Created specsync.json" , "✓" . green( ) ) ;
100104}
101105
102- fn cmd_check ( root : & Path , strict : bool , require_coverage : Option < usize > ) {
106+ fn cmd_check ( root : & Path , strict : bool , require_coverage : Option < usize > , json : bool ) {
103107 let ( config, spec_files) = load_and_discover ( root) ;
104108 let schema_tables = get_schema_table_names ( root, & config) ;
105- let ( total_errors, total_warnings, passed, total) =
106- run_validation ( root, & spec_files, & schema_tables, & config) ;
109+ let ( total_errors, total_warnings, passed, total, all_errors , all_warnings ) =
110+ run_validation ( root, & spec_files, & schema_tables, & config, json ) ;
107111 let coverage = compute_coverage ( root, & spec_files, & config) ;
108112
113+ if json {
114+ let exit_code = compute_exit_code (
115+ total_errors,
116+ total_warnings,
117+ strict,
118+ & coverage,
119+ require_coverage,
120+ ) ;
121+ let output = serde_json:: json!( {
122+ "passed" : exit_code == 0 ,
123+ "errors" : all_errors,
124+ "warnings" : all_warnings,
125+ "specs_checked" : total,
126+ } ) ;
127+ println ! ( "{}" , serde_json:: to_string_pretty( & output) . unwrap( ) ) ;
128+ process:: exit ( exit_code) ;
129+ }
130+
109131 print_summary ( total, passed, total_warnings, total_errors) ;
110132 print_coverage_line ( & coverage) ;
111133 exit_with_status (
@@ -117,13 +139,36 @@ fn cmd_check(root: &Path, strict: bool, require_coverage: Option<usize>) {
117139 ) ;
118140}
119141
120- fn cmd_coverage ( root : & Path , strict : bool , require_coverage : Option < usize > ) {
142+ fn cmd_coverage ( root : & Path , strict : bool , require_coverage : Option < usize > , json : bool ) {
121143 let ( config, spec_files) = load_and_discover ( root) ;
122144 let schema_tables = get_schema_table_names ( root, & config) ;
123- let ( total_errors, total_warnings, passed, total) =
124- run_validation ( root, & spec_files, & schema_tables, & config) ;
145+ let ( total_errors, total_warnings, passed, total, _all_errors , _all_warnings ) =
146+ run_validation ( root, & spec_files, & schema_tables, & config, json ) ;
125147 let coverage = compute_coverage ( root, & spec_files, & config) ;
126148
149+ if json {
150+ let file_coverage = if coverage. total_source_files == 0 {
151+ 100.0
152+ } else {
153+ ( coverage. specced_file_count as f64 / coverage. total_source_files as f64 ) * 100.0
154+ } ;
155+
156+ let modules: Vec < serde_json:: Value > = coverage
157+ . unspecced_modules
158+ . iter ( )
159+ . map ( |m| serde_json:: json!( { "name" : m, "has_spec" : false } ) )
160+ . collect ( ) ;
161+
162+ let output = serde_json:: json!( {
163+ "file_coverage" : ( file_coverage * 100.0 ) . round( ) / 100.0 ,
164+ "files_covered" : coverage. specced_file_count,
165+ "files_total" : coverage. total_source_files,
166+ "modules" : modules,
167+ } ) ;
168+ println ! ( "{}" , serde_json:: to_string_pretty( & output) . unwrap( ) ) ;
169+ process:: exit ( 0 ) ;
170+ }
171+
127172 print_coverage_report ( & coverage) ;
128173 print_summary ( total, passed, total_warnings, total_errors) ;
129174 print_coverage_line ( & coverage) ;
@@ -136,13 +181,22 @@ fn cmd_coverage(root: &Path, strict: bool, require_coverage: Option<usize>) {
136181 ) ;
137182}
138183
139- fn cmd_generate ( root : & Path , strict : bool , require_coverage : Option < usize > ) {
184+ fn cmd_generate ( root : & Path , strict : bool , require_coverage : Option < usize > , json : bool ) {
140185 let ( config, spec_files) = load_and_discover ( root) ;
141186 let schema_tables = get_schema_table_names ( root, & config) ;
142- let ( total_errors, total_warnings, passed, total) =
143- run_validation ( root, & spec_files, & schema_tables, & config) ;
187+ let ( total_errors, total_warnings, passed, total, _all_errors , _all_warnings ) =
188+ run_validation ( root, & spec_files, & schema_tables, & config, json ) ;
144189 let coverage = compute_coverage ( root, & spec_files, & config) ;
145190
191+ if json {
192+ let generated_paths = generate_specs_for_unspecced_modules_paths ( root, & coverage, & config) ;
193+ let output = serde_json:: json!( {
194+ "generated" : generated_paths,
195+ } ) ;
196+ println ! ( "{}" , serde_json:: to_string_pretty( & output) . unwrap( ) ) ;
197+ process:: exit ( 0 ) ;
198+ }
199+
146200 print_coverage_report ( & coverage) ;
147201
148202 println ! (
@@ -196,19 +250,34 @@ fn load_and_discover(root: &Path) -> (types::SpecSyncConfig, Vec<PathBuf>) {
196250 ( config, spec_files)
197251}
198252
253+ /// Run validation, returning counts and collected error/warning strings.
199254fn run_validation (
200255 root : & Path ,
201256 spec_files : & [ PathBuf ] ,
202257 schema_tables : & std:: collections:: HashSet < String > ,
203258 config : & types:: SpecSyncConfig ,
204- ) -> ( usize , usize , usize , usize ) {
259+ json : bool ,
260+ ) -> ( usize , usize , usize , usize , Vec < String > , Vec < String > ) {
205261 let mut total_errors = 0 ;
206262 let mut total_warnings = 0 ;
207263 let mut passed = 0 ;
264+ let mut all_errors: Vec < String > = Vec :: new ( ) ;
265+ let mut all_warnings: Vec < String > = Vec :: new ( ) ;
208266
209267 for spec_file in spec_files {
210268 let result = validate_spec ( spec_file, root, schema_tables, config) ;
211269
270+ if json {
271+ all_errors. extend ( result. errors . iter ( ) . cloned ( ) ) ;
272+ all_warnings. extend ( result. warnings . iter ( ) . cloned ( ) ) ;
273+ total_errors += result. errors . len ( ) ;
274+ total_warnings += result. warnings . len ( ) ;
275+ if result. errors . is_empty ( ) {
276+ passed += 1 ;
277+ }
278+ continue ;
279+ }
280+
212281 println ! ( "\n {}" , result. spec_path. bold( ) ) ;
213282
214283 // Frontmatter check
@@ -334,7 +403,36 @@ fn run_validation(
334403 }
335404 }
336405
337- ( total_errors, total_warnings, passed, spec_files. len ( ) )
406+ (
407+ total_errors,
408+ total_warnings,
409+ passed,
410+ spec_files. len ( ) ,
411+ all_errors,
412+ all_warnings,
413+ )
414+ }
415+
416+ /// Compute exit code without printing or exiting.
417+ fn compute_exit_code (
418+ total_errors : usize ,
419+ total_warnings : usize ,
420+ strict : bool ,
421+ coverage : & types:: CoverageReport ,
422+ require_coverage : Option < usize > ,
423+ ) -> i32 {
424+ if total_errors > 0 {
425+ return 1 ;
426+ }
427+ if strict && total_warnings > 0 {
428+ return 1 ;
429+ }
430+ if let Some ( req) = require_coverage
431+ && coverage. coverage_percent < req
432+ {
433+ return 1 ;
434+ }
435+ 0
338436}
339437
340438fn print_summary ( total : usize , passed : usize , warnings : usize , _errors : usize ) {
0 commit comments