Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":

Expand Down
86 changes: 86 additions & 0 deletions diagnostic/build-16b0f8d9.json
Original file line number Diff line number Diff line change
@@ -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 <outdir> --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."
}
Binary file added diagnostic/build-16b0f8d9.logd
Binary file not shown.
47 changes: 39 additions & 8 deletions tools/ai_reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -398,31 +411,31 @@ 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,
},
{
"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,
},
{
"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,
},
{
"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,
Expand Down Expand Up @@ -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),
Expand All @@ -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 = [
Expand All @@ -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:
Expand Down Expand Up @@ -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


Expand All @@ -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}")
Expand Down Expand Up @@ -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}")
Expand Down
43 changes: 43 additions & 0 deletions tools/validate_ai_reviewer_ignore_extensions.py
Original file line number Diff line number Diff line change
@@ -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())
Loading