Repository navigation
Expand file tree
/
Copy pathvalidate_files.py
More file actions
186 lines (142 loc) · 5.96 KB
/
Copy pathvalidate_files.py
File metadata and controls
186 lines (142 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python3
"""
Best-effort syntax validation for files touched by today's AI agents.
Reads .agent_state.json for the list of files modified today and checks
each one with whatever lightweight tool is available for its language.
BLOCKING checks (the specific failing file is reverted - see below):
.py -> python3 -m py_compile
.js -> node --check
.go -> gofmt -l (parses syntax; doesn't need a go.mod)
.cpp -> g++ -fsyntax-only
.rb -> ruby -c (syntax check only, no gem dependencies needed)
ADVISORY checks (printed as a warning only, never reverts anything):
.sql -> sqlite3 parse (schema.sql may use dialect features sqlite
rejects even when valid for the intended database, so this is
informational rather than authoritative)
.rs -> rustc --emit=metadata, no Cargo.toml/dependencies available.
Real on-chain Rust (OnChainProgram.rs) depends on external
crates like solana_program and borsh - a bare rustc check has
no way to resolve those, so it will always report "cannot find
crate" and cascading errors for legitimate, correct code.
NOT CHECKED (no lightweight tool available without heavier CI setup):
.cs (C# - would need a scaffolded .csproj to compile a single file)
.sol (Solidity - would need solc + resolved imports, e.g. OpenZeppelin,
to avoid false failures on legitimate contracts)
.html/.css/.md - not code in the sense that matters here
REVERT BEHAVIOR: a file that fails a blocking check is reverted rather
than left broken or allowed to block the whole run's commit:
- If the file already existed before today's run, it's restored to its
last known-good committed version (git checkout -- <path>).
- If the file is brand new (never committed before), it's simply
deleted so it's never added.
This script always exits 0 - it cleans up failures itself instead of
stopping the pipeline, so one agent's bad output only costs that one
file's progress for the day, not every other agent's valid work too.
"""
import os
import json
import subprocess
import sys
STATE_FILE = ".agent_state.json"
def get_todays_files():
if not os.path.exists(STATE_FILE):
return []
try:
with open(STATE_FILE, "r") as f:
return json.load(f).get("modified_files", [])
except Exception:
return []
def run(cmd):
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
return result.returncode, (result.stdout + result.stderr).strip()
except FileNotFoundError:
return None, f"tool not installed: {cmd[0]}"
except subprocess.TimeoutExpired:
return -1, "validation timed out"
def validate_python(path):
return run(["python3", "-m", "py_compile", path])
def validate_js(path):
return run(["node", "--check", path])
def validate_go(path):
return run(["gofmt", "-l", path])
def validate_cpp(path):
return run(["g++", "-fsyntax-only", "-std=c++17", path])
def validate_ruby(path):
return run(["ruby", "-c", path])
def validate_rust_advisory(path):
return run(["rustc", "--edition", "2021", "--crate-type", "lib",
"--emit=metadata", "-o", "/dev/null", path])
def validate_sql_advisory(path):
return run(["sqlite3", ":memory:", f".read {path}"])
BLOCKING_VALIDATORS = {
".py": validate_python,
".js": validate_js,
".go": validate_go,
".cpp": validate_cpp,
".rb": validate_ruby,
}
ADVISORY_VALIDATORS = {
".sql": validate_sql_advisory,
".rs": validate_rust_advisory,
}
def file_existed_before_today(path):
"""True if this path already had a committed version before today's
run (i.e. it's at HEAD) - used to decide revert vs delete."""
result = subprocess.run(
["git", "cat-file", "-e", f"HEAD:{path}"],
capture_output=True
)
return result.returncode == 0
def revert_file(path):
if file_existed_before_today(path):
subprocess.run(["git", "checkout", "--", path], check=False)
print(f" -> Reverted {path} to its last known-good committed version.")
else:
try:
os.remove(path)
print(f" -> Removed {path} (it was new today, never previously committed).")
except FileNotFoundError:
pass
def main():
files = get_todays_files()
if not files:
print("No files recorded in .agent_state.json - nothing to validate.")
return 0
failing_paths = []
for path in files:
if not os.path.exists(path):
print(f"[SKIP] {path}: file not found.")
continue
ext = os.path.splitext(path)[1]
if ext in BLOCKING_VALIDATORS:
code, output = BLOCKING_VALIDATORS[ext](path)
if code is None:
print(f"[SKIP] {path}: {output}")
elif code == 0 and not output:
print(f"[OK] {path}")
elif code == 0 and output:
# e.g. gofmt -l prints the filename itself when formatting
# differs but syntax is still valid - not a hard failure.
print(f"[OK] {path} (tool printed output, but exit code 0): {output}")
else:
print(f"[FAIL] {path}:\n{output}")
failing_paths.append(path)
elif ext in ADVISORY_VALIDATORS:
code, output = ADVISORY_VALIDATORS[ext](path)
if code not in (0, None):
print(f"[WARN] {path} (advisory only, not blocking):\n{output}")
else:
print(f"[OK] {path} (advisory check)")
else:
print(f"[SKIP] {path}: no validator for '{ext}' files.")
if failing_paths:
print(f"\n{len(failing_paths)} file(s) failed validation. Reverting just those, "
f"so today's other valid changes still commit normally:")
for path in failing_paths:
revert_file(path)
else:
print("\nAll validated files passed.")
return 0
if __name__ == "__main__":
sys.exit(main())