diff --git a/build.py b/build.py index 0116f883..35af98eb 100644 --- a/build.py +++ b/build.py @@ -288,6 +288,8 @@ def build_module( return False, time.time() - start, f"npm install failed:\n{install_result.stderr}" except subprocess.TimeoutExpired: return False, time.time() - start, "npm install TIMEOUT (120s)" + except FileNotFoundError as e: + return False, 0, f"Command not found: {e}" if module.name == "engine": diff --git a/diagnostic/build-16b0f8d9.json b/diagnostic/build-16b0f8d9.json new file mode 100644 index 00000000..2eeff331 --- /dev/null +++ b/diagnostic/build-16b0f8d9.json @@ -0,0 +1,86 @@ +{ + "generated_at": "2026-06-19T19:44:16.263880+00:00", + "commit": "16b0f8d9", + "diagnostic_logd": "diagnostic\\build-16b0f8d9.logd", + "diagnostic_logd_error": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "92e9fa256b00732c5fb0", + "decrypt_command": "encryptly unpack diagnostic\\build-16b0f8d9.logd --password 92e9fa256b00732c5fb0", + "total_modules": 10, + "passed": 0, + "failed": 10, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "frontend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "frailbox", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [WinError 2] The system cannot find the file specified" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic\\build-16b0f8d9.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-16b0f8d9.logd b/diagnostic/build-16b0f8d9.logd new file mode 100644 index 00000000..65acdb4d Binary files /dev/null and b/diagnostic/build-16b0f8d9.logd differ diff --git a/tools/ai_reviewer.py b/tools/ai_reviewer.py index bfd85722..bc099a5f 100644 --- a/tools/ai_reviewer.py +++ b/tools/ai_reviewer.py @@ -103,6 +103,19 @@ DEFAULT_MAX_FILE_LENGTH = 500 DEFAULT_MAX_PARAMS = 5 + +def normalize_ignore_extensions(raw: str) -> Set[str]: + """Normalize a comma-separated extension list to lowercase dotted suffixes.""" + ignored: Set[str] = set() + for item in raw.split(","): + ext = item.strip().lower() + if not ext: + continue + if not ext.startswith("."): + ext = f".{ext}" + ignored.add(ext) + return ignored + # --------------------------------------------------------------------------- # Types # --------------------------------------------------------------------------- @@ -398,7 +411,7 @@ def _initialize_patterns(self) -> List[Dict[str, Any]]: { "id": "SEC-PATH-TRAVERSAL", "name": "Path Traversal", - "severity": ReviewSeverity.HIGH, + "severity": ReviewSeverity.ERROR, "pattern": r"(open|read|write|unlink|rmdir|Path::new)\s*\(\s*['\"](\.\./|/etc/|/var/)", "message": "Possible path traversal vulnerability. Validate file paths.", "effort": 20, @@ -406,7 +419,7 @@ def _initialize_patterns(self) -> List[Dict[str, Any]]: { "id": "SEC-INSECURE-RANDOM", "name": "Insecure Random Number Generator", - "severity": ReviewSeverity.HIGH, + "severity": ReviewSeverity.ERROR, "pattern": r"(random\.randint|random\.choice|srand|rand\(\)|math\.random)", "message": "Use cryptographically secure random generation for security-sensitive contexts.", "effort": 10, @@ -414,7 +427,7 @@ def _initialize_patterns(self) -> List[Dict[str, Any]]: { "id": "SEC-INSECURE-COOKIE", "name": "Insecure Cookie Configuration", - "severity": ReviewSeverity.HIGH, + "severity": ReviewSeverity.ERROR, "pattern": r"cookie\s*[\[=]\s*.*\b(httpOnly|secure|sameSite)\b\s*[=:]\s*(false|False|None)", "message": "Insecure cookie configuration. Set HttpOnly, Secure, and SameSite attributes.", "effort": 10, @@ -422,7 +435,7 @@ def _initialize_patterns(self) -> List[Dict[str, Any]]: { "id": "SEC-XXE", "name": "XML External Entity (XXE)", - "severity": ReviewSeverity.HIGH, + "severity": ReviewSeverity.ERROR, "pattern": r"(xml\.etree|xml_parser|parse\(|SAXParser|DocumentBuilder)", "message": "Possible XXE vulnerability. Disable external entity parsing.", "effort": 20, @@ -701,8 +714,14 @@ def review_file(self, path: Path) -> FileReviewResult: self.logger.info(result.summary) return result - def review_directory(self, path: Path, recursive: bool = True) -> ProjectReviewReport: + def review_directory( + self, + path: Path, + recursive: bool = True, + ignore_extensions: Optional[Set[str]] = None, + ) -> ProjectReviewReport: """Review all supported files in a directory.""" + ignored = {ext.lower() for ext in (ignore_extensions or set())} report = ProjectReviewReport( timestamp=datetime.now().isoformat(), project_path=str(path), @@ -717,10 +736,11 @@ def review_directory(self, path: Path, recursive: bool = True) -> ProjectReviewR ) # Collect files + review_extensions = REVIEW_EXTENSIONS - ignored if recursive: - files = [f for ext in REVIEW_EXTENSIONS for f in path.rglob(f"*{ext}")] + files = [f for ext in review_extensions for f in path.rglob(f"*{ext}")] else: - files = [f for ext in REVIEW_EXTENSIONS for f in path.glob(f"*{ext}")] + files = [f for ext in review_extensions for f in path.glob(f"*{ext}")] # Exclude common generated/vendor directories files = [ @@ -733,6 +753,8 @@ def review_directory(self, path: Path, recursive: bool = True) -> ProjectReviewR ] report.total_files = len(files) + if ignored: + self.logger.info(f"Ignoring extensions: {', '.join(sorted(ignored))}") self.logger.info(f"Found {len(files)} files to review") for file_path in files: @@ -795,6 +817,11 @@ def create_parser() -> argparse.ArgumentParser: parser.add_argument("--path", type=str, required=True, help="File or directory to review") parser.add_argument("--recursive", action="store_true", help="Review directories recursively") parser.add_argument("--output", type=str, default=None, help="Output JSON report path") + parser.add_argument( + "--ignore-extensions", + default="", + help="Comma-separated file extensions to skip during directory review, for example .md,.txt", + ) return parser @@ -804,8 +831,12 @@ def main() -> int: reviewer = AiCodeReviewer() path = Path(args.path) + ignore_extensions = normalize_ignore_extensions(args.ignore_extensions) if path.is_file(): + if path.suffix.lower() in ignore_extensions: + logger.info(f"Skipping {path}; extension is ignored") + return 0 result = reviewer.review_file(path) print(f"\n{'='*60}") print(f"AI Code Review: {path}") @@ -836,7 +867,7 @@ def main() -> int: print() elif path.is_dir(): - report = reviewer.review_directory(path, args.recursive) + report = reviewer.review_directory(path, args.recursive, ignore_extensions=ignore_extensions) print(f"\n{'='*60}") print(f"AI Project Review: {path}") print(f"{'='*60}") diff --git a/tools/validate_ai_reviewer_ignore_extensions.py b/tools/validate_ai_reviewer_ignore_extensions.py new file mode 100644 index 00000000..dff783d8 --- /dev/null +++ b/tools/validate_ai_reviewer_ignore_extensions.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Smoke checks for ai_reviewer.py ignored extension filtering.""" + +from __future__ import annotations + +from pathlib import Path +import sys +import tempfile + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools")) + +import ai_reviewer # noqa: E402 + + +def require(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def main() -> int: + ignored = ai_reviewer.normalize_ignore_extensions("py, .TS ,") + require(ignored == {".py", ".ts"}, str(ignored)) + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "keep.rs").write_text("fn main() {}\n", encoding="utf-8") + (root / "skip.py").write_text("print('skip me')\n", encoding="utf-8") + (root / "skip.ts").write_text("export const skip = true;\n", encoding="utf-8") + + reviewer = ai_reviewer.AiCodeReviewer() + report = reviewer.review_directory(root, recursive=False, ignore_extensions=ignored) + reviewed_paths = {Path(result.file_path).name for result in report.file_results} + require(report.total_files == 1, str(report.total_files)) + require(reviewed_paths == {"keep.rs"}, str(reviewed_paths)) + + print("ai_reviewer ignore extension checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main())