Skip to content

Commit 49bf2db

Browse files
ci: gate import-scale and unevaluated-export anti-patterns
Static scan of snippets and templates so the two new rules fail on a deliberate canary, not only on good input. examples/ is excluded because pathology witnesses are intentional. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 190d2b2 commit 49bf2db

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

.github/workflows/validate.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ jobs:
130130
fi
131131
echo "All snippets have valid Python syntax."
132132
133+
- name: Check import-scale and unevaluated-export anti-patterns
134+
run: python3 tests/check_import_export_rules.py
135+
133136
- name: Validate template Python syntax
134137
run: |
135138
echo "Checking template Python syntax..."

tests/check_import_export_rules.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Static checks for validate-imported-mesh-scale and
2+
no-unapplied-modifiers-on-export.
3+
4+
Scans snippets/ and templates/**/*.py. examples/ is excluded because several
5+
examples are intentional pathology witnesses (unapplied-scale-gltf).
6+
"""
7+
import glob
8+
import os
9+
import re
10+
import sys
11+
12+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13+
14+
REQUIRED_RULES = (
15+
"rules/validate-imported-mesh-scale.mdc",
16+
"rules/no-unapplied-modifiers-on-export.mdc",
17+
)
18+
19+
IMPORT_RE = re.compile(r"bpy\.ops\.import_scene\.(gltf|fbx)\s*\(")
20+
EXPORT_RE = re.compile(
21+
r"bpy\.ops\.(export_scene\.gltf|export_scene\.fbx|wm\.usd_export)\s*\("
22+
)
23+
MESH_WORK_RE = re.compile(r"bmesh\.|modifiers\.new|from_pydata|foreach_set")
24+
UNIT_SCALE_RE = re.compile(r"unit_settings|scale_length|global_scale")
25+
EXPORT_EVAL_RE = re.compile(
26+
r"export_apply\s*=\s*True|evaluation_mode\s*=|use_mesh_modifiers\s*=\s*True"
27+
)
28+
MODIFIER_NEW_RE = re.compile(r"modifiers\.new")
29+
MODIFIER_APPLY_RE = re.compile(r"modifier_apply")
30+
31+
32+
def scan_paths(extra):
33+
paths = []
34+
paths.extend(glob.glob(os.path.join(ROOT, "snippets", "*.py")))
35+
paths.extend(
36+
glob.glob(os.path.join(ROOT, "templates", "**", "*.py"), recursive=True)
37+
)
38+
for item in extra:
39+
paths.append(item if os.path.isabs(item) else os.path.join(ROOT, item))
40+
return paths
41+
42+
43+
def check_text(rel, text):
44+
errors = []
45+
if IMPORT_RE.search(text) and MESH_WORK_RE.search(text):
46+
if "transform_apply" not in text or not UNIT_SCALE_RE.search(text):
47+
errors.append(
48+
f"{rel}: import_scene gltf/fbx plus mesh work without "
49+
"transform_apply and a unit-scale check "
50+
"(unit_settings, scale_length, or global_scale)"
51+
)
52+
if EXPORT_RE.search(text) and MODIFIER_NEW_RE.search(text):
53+
if not EXPORT_EVAL_RE.search(text) and not MODIFIER_APPLY_RE.search(text):
54+
errors.append(
55+
f"{rel}: export with modifiers.new but no export_apply=True, "
56+
"evaluation_mode, or modifier_apply"
57+
)
58+
return errors
59+
60+
61+
def main(argv):
62+
errors = []
63+
for rule in REQUIRED_RULES:
64+
path = os.path.join(ROOT, rule)
65+
if not os.path.isfile(path):
66+
errors.append(f"missing rule file {rule}")
67+
68+
extra = argv[1:]
69+
for path in scan_paths(extra):
70+
if not os.path.isfile(path):
71+
errors.append(f"missing scan path {path}")
72+
continue
73+
rel = os.path.relpath(path, ROOT).replace("\\", "/")
74+
text = open(path, encoding="utf-8").read()
75+
errors.extend(check_text(rel, text))
76+
77+
if errors:
78+
for err in errors:
79+
print(f"ERROR: {err}", file=sys.stderr)
80+
return 1
81+
print("import/export anti-pattern checks passed.")
82+
return 0
83+
84+
85+
if __name__ == "__main__":
86+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)