@@ -508,6 +508,74 @@ async def run_linting_gate(self, task: Task, task_category: Optional[str] = None
508508
509509 return result
510510
511+ async def run_build_gate (self , task : Task , task_category : Optional [str ] = None ) -> QualityGateResult :
512+ """Execute build validation gate - verify project configuration files are valid.
513+
514+ This gate validates that pyproject.toml and/or package.json can be parsed
515+ and dependencies resolved, catching configuration errors before slower gates run.
516+
517+ Detection Logic:
518+ - If pyproject.toml exists → run uv sync --no-install-project (fallback: pip)
519+ - If package.json exists → run npm install --dry-run --ignore-scripts
520+
521+ Args:
522+ task: Task to validate
523+ task_category: Optional category string for logging
524+
525+ Returns:
526+ QualityGateResult with pass/fail status
527+ """
528+ start_time = datetime .now (timezone .utc )
529+ failures : List [QualityGateFailure ] = []
530+
531+ has_python = (self .project_root / "pyproject.toml" ).exists ()
532+ has_node = (self .project_root / "package.json" ).exists ()
533+
534+ if has_python :
535+ build_result = self ._run_python_build ()
536+ if build_result ["returncode" ] != 0 :
537+ failures .append (
538+ QualityGateFailure (
539+ gate = QualityGateType .BUILD ,
540+ reason = f"Build validation failed for pyproject.toml: { build_result ['summary' ]} " ,
541+ details = build_result ["output" ],
542+ severity = Severity .HIGH ,
543+ )
544+ )
545+
546+ if has_node :
547+ build_result = self ._run_node_build ()
548+ if build_result ["returncode" ] != 0 :
549+ failures .append (
550+ QualityGateFailure (
551+ gate = QualityGateType .BUILD ,
552+ reason = f"Build validation failed for package.json: { build_result ['summary' ]} " ,
553+ details = build_result ["output" ],
554+ severity = Severity .HIGH ,
555+ )
556+ )
557+
558+ execution_time = (datetime .now (timezone .utc ) - start_time ).total_seconds ()
559+
560+ status = "passed" if len (failures ) == 0 else "failed"
561+ result = QualityGateResult (
562+ task_id = task .id , # type: ignore[arg-type]
563+ status = status ,
564+ failures = failures ,
565+ execution_time_seconds = execution_time ,
566+ )
567+
568+ self .db .update_quality_gate_status (
569+ task_id = task .id , # type: ignore[arg-type]
570+ status = status ,
571+ failures = failures ,
572+ )
573+
574+ if not result .passed :
575+ self ._create_quality_blocker (task , failures , task_category )
576+
577+ return result
578+
511579 async def run_skip_detection_gate (self , task : Task , task_category : Optional [str ] = None ) -> QualityGateResult :
512580 """Execute skip pattern detection gate - detect test skips across all languages.
513581
@@ -720,7 +788,16 @@ async def run_all_gates(self, task: Task) -> QualityGateResult:
720788 logger .info (f"Skipping linting gate for task { task .id } : { reason } " )
721789 skipped_gates .append (QualityGateType .LINTING .value )
722790
723- # 2. Type check gate (fast)
791+ # 2. Build gate (fast, validates configuration)
792+ if QualityGateType .BUILD in applicable_gates :
793+ build_result = await self .run_build_gate (task , category .value )
794+ all_failures .extend (build_result .failures )
795+ else :
796+ reason = rules .get_skip_reason (category , QualityGateType .BUILD )
797+ logger .info (f"Skipping build gate for task { task .id } : { reason } " )
798+ skipped_gates .append (QualityGateType .BUILD .value )
799+
800+ # 3. Type check gate (fast)
724801 if QualityGateType .TYPE_CHECK in applicable_gates :
725802 type_check_result = await self .run_type_check_gate (task , category .value )
726803 all_failures .extend (type_check_result .failures )
@@ -1092,10 +1169,81 @@ def _run_eslint(self) -> Dict[str, Any]:
10921169 "summary" : "Skipped" ,
10931170 }
10941171
1172+ def _run_python_build (self ) -> Dict [str , Any ]:
1173+ """Run Python build validation using uv (fallback to pip)."""
1174+ try :
1175+ result = subprocess .run (
1176+ ["uv" , "sync" , "--no-install-project" ],
1177+ cwd = str (self .project_root ),
1178+ capture_output = True ,
1179+ text = True ,
1180+ timeout = 60 ,
1181+ )
1182+ output = result .stdout + result .stderr
1183+ summary = self ._extract_build_error_summary (output , "pyproject.toml" )
1184+ return {"returncode" : result .returncode , "output" : output , "summary" : summary }
1185+ except FileNotFoundError :
1186+ # uv not available, try pip
1187+ try :
1188+ result = subprocess .run (
1189+ ["pip" , "install" , "-e" , "." , "--dry-run" ],
1190+ cwd = str (self .project_root ),
1191+ capture_output = True ,
1192+ text = True ,
1193+ timeout = 60 ,
1194+ )
1195+ output = result .stdout + result .stderr
1196+ summary = self ._extract_build_error_summary (output , "pyproject.toml" )
1197+ return {"returncode" : result .returncode , "output" : output , "summary" : summary }
1198+ except FileNotFoundError :
1199+ return {"returncode" : 0 , "output" : "No Python build tool found, skipping" , "summary" : "Skipped" }
1200+ except subprocess .TimeoutExpired :
1201+ return {"returncode" : 1 , "output" : "pip install timed out after 60 seconds" , "summary" : "Timeout" }
1202+ except subprocess .TimeoutExpired :
1203+ return {"returncode" : 1 , "output" : "Build validation timed out after 60 seconds" , "summary" : "Timeout" }
1204+
1205+ def _run_node_build (self ) -> Dict [str , Any ]:
1206+ """Run Node.js build validation using npm."""
1207+ try :
1208+ result = subprocess .run (
1209+ ["npm" , "install" , "--dry-run" , "--ignore-scripts" ],
1210+ cwd = str (self .project_root ),
1211+ capture_output = True ,
1212+ text = True ,
1213+ timeout = 60 ,
1214+ )
1215+ output = result .stdout + result .stderr
1216+ summary = self ._extract_build_error_summary (output , "package.json" )
1217+ return {"returncode" : result .returncode , "output" : output , "summary" : summary }
1218+ except FileNotFoundError :
1219+ return {"returncode" : 0 , "output" : "npm not found, skipping" , "summary" : "Skipped" }
1220+ except subprocess .TimeoutExpired :
1221+ return {"returncode" : 1 , "output" : "npm install timed out after 60 seconds" , "summary" : "Timeout" }
1222+
10951223 # ========================================================================
10961224 # Helper Methods - Output Parsing
10971225 # ========================================================================
10981226
1227+ def _extract_build_error_summary (self , output : str , config_file : str ) -> str :
1228+ """Extract summary from build validation output."""
1229+ if not output .strip ():
1230+ return "No errors"
1231+ # Detect common build error patterns
1232+ if "hatchling" in output .lower ():
1233+ match = re .search (r"Invalid section \[([^\]]+)\]" , output )
1234+ if match :
1235+ return f"Invalid section [{ match .group (1 )} ] in { config_file } "
1236+ return f"hatchling build error in { config_file } "
1237+ if "invalid toml" in output .lower () or "failed to parse" in output .lower ():
1238+ return f"Invalid TOML syntax in { config_file } "
1239+ if "invalid package.json" in output .lower () or "invalid json" in output .lower ():
1240+ return f"Invalid JSON in { config_file } "
1241+ if "npm err" in output .lower ():
1242+ return f"npm dependency resolution error in { config_file } "
1243+ # Generic fallback
1244+ first_line = output .strip ().split ("\n " )[0 ][:120 ]
1245+ return first_line
1246+
10991247 def _extract_pytest_summary (self , output : str ) -> str :
11001248 """Extract summary from pytest output."""
11011249 # Look for "N passed, M failed" pattern
@@ -1246,7 +1394,7 @@ def _get_category_guidance(self, task_category: str) -> str:
12461394 ),
12471395 "configuration" : (
12481396 "This task was classified as a configuration task. "
1249- "Only linting and type checking gates apply."
1397+ "Build validation, linting, and type checking gates apply."
12501398 ),
12511399 "testing" : (
12521400 "This task was classified as a testing task. "
0 commit comments